A look at the data

An ongoing project in the Simpson Desert of central Australia is producing genomic sequence data for small mammals and reptiles collected over more than 30 years. We have some preliminary data from this project for the Sandy Inland Mouse (Pseudomys hermannsburgensis) that we’d like to use to explore, through data analysis and simulation, and hopefully gain some insight into where this project might ultimately take us. First let’s load all the packages we use in the example, and then load the genetic data we will be working with. The data is stored in an .Rdata file containing a genlight object, which stores binary Single Nucleotide Polymorphism (SNP) data, defined in the adegenet package. We also have demographic data in the form of trap capture data (captures per 100 trap nights; see Dickman et al. 2014 and Greenville et al. 2016 for capture methods).

library(readr)
library(dplyr)
library(tidyr)
library(tibble)
library(mapview)
library(purrr)
library(conflicted)
library(future)
library(furrr)
library(lubridate)
library(ggplot2)
library(ggforce)
library(gganimate)
library(directlabels)
library(patchwork)
library(sf)
library(adegenet)
library(here)
library(stringr)

## install dependencies for dartR if you don't have them
if (!require("SNPRelate", quietly = TRUE)) {
  if (!require("BiocManager", quietly = TRUE))
    install.packages("BiocManager")
  BiocManager::install("SNPRelate")
  if (!require("dartR", quietly = TRUE))
    install.packages("dartR")
}

## install slimr if you don't have it already
if (!require("slimr", quietly = TRUE)) {
  if (!require("devtools", quietly = TRUE))
    install.packages("devtools")
  devtools::install_github("rdinnager/slimr")
}

library(dartR)
library(slimr)

mapviewOptions(fgb = FALSE)

## because filter function exists in multiple packages we use
## the conflicted package to make sure we R uses the version in dplyr
conflict_prefer("filter", "dplyr") 
conflict_prefer("select", "dplyr")
conflict_prefer("initialize", "slimr")

## load snp data using function load_herm() from slimr package
gen <- load_herm()

gen
##  /// GENLIGHT OBJECT /////////
## 
##  // 167 genotypes,  39,978 binary SNPs, size: 28.4 Mb
##  1028248 (15.4 %) missing data
## 
##  // Basic content
##    @gen: list of 167 SNPbin
##    @ploidy: ploidy of each individual  (range: 2-2)
## 
##  // Optional content
##    @ind.names:  167 individual labels
##    @loc.names:  39978 locus labels
##    @loc.all:  39978 alleles
##    @position: integer storing positions of the SNPs
##    @pop: population of each individual (group size range: 2-39)
##    @other: a list containing: loc.metrics  latlong  ind.metrics  loc.metrics.flags  verbose  history
abund_summ <- read_csv(system.file("extdata/hermannsburg_abund.csv", package = "slimr"))

abund_summ
## # A tibble: 408 × 3
##    date       pop    abund
##    <date>     <chr>  <dbl>
##  1 1990-03-01 MC     0.525
##  2 1990-06-01 MC     0.926
##  3 1990-08-01 MC     0.794
##  4 1990-09-01 MC     3.24 
##  5 1991-04-01 MC     4.17 
##  6 1991-05-01 MC     3.11 
##  7 1991-07-01 MC    15.4  
##  8 1991-08-01 MC     8.73 
##  9 1991-10-01 MC     2.28 
## 10 1991-11-01 MC     6.94 
## # ℹ 398 more rows

So we have SNP sequences from 167 individuals taken over three years and several different sites. Before we take a look at the genetic data, let’s set the environmental context a bit. These small mammals live in the Simpson Desert, an arid ecosystem characterized by long periods of little rain, with occasional major rainfall events. These tend to occur every 6-10 years. After rainfall events, the desert turns green, it can be seen from space, and our analysis of satellite reflectance data confirms massive spikes in productivity after these rainfalls. Plants grow, flower and seed massively during these periods, and so food is abundant for small mammals. We can see the regular pulse of rainfall just by looking at the mammal abundances – Pseudomys hermannsburgensis in particular responds quickly to rainfall. Let’s look at our live trapping data on abundances to see this pattern.

ggplot(abund_summ, aes(date, abund)) +
  geom_path(aes(colour = pop)) +
  geom_dl(aes(label = pop), method = "last.qp") +
  aes(colour = pop) +
  theme_minimal() +
  theme(legend.position = "none")

Wow, those pulses are quite clear and quite extreme!

Okay, now let’s go have a look at the SNP data we have for these populations.

First, we will extract the metadata for the genetic samples and have a look at how they are distributed in the Simpson Desert.

sites <- tribble(~pop, ~SiteName,
                 "CS", "Carlo",
                 "FRN", "Field River North",
                 "FRS", "Field River South",
                 "KSE", "Kunnamuka Swamp East",
                 "MC", "Main Camp",
                 "SS", "South Site",
                 "WS", "Way Site"
)

gen_meta <- gen@other$ind.metrics

gen_meta <- gen_meta %>%
  as_tibble() %>%
  ## fix some minor errors in genetic metadata with respect to site names:
  mutate(pop = case_when(pop == "FR" ~ substr(as.character(SiteGrid), 1, 3),
                         pop == "KS" ~ "KSE",
                         TRUE ~ as.character(pop))) %>%
  left_join(sites)
## Joining with `by = join_by(pop)`
gen_meta
## # A tibble: 167 × 18
##    id         pop     id2 Date     year month timem Species SiteGrid Trap  Sex  
##    <chr>      <chr> <int> <fct>   <int> <int> <int> <fct>   <fct>    <fct> <fct>
##  1 P_herm_121 KSE     121 16/05/…  2006     5     1 Pseudo… KSEAS    AS.32 "F"  
##  2 P_herm_193 FRN     193 17/09/…  2007     9    17 Pseudo… FRN1     1.09  "F"  
##  3 P_herm_201 KSE     201 13/09/…  2007     9    17 Pseudo… KSE1     1.28  "F"  
##  4 P_herm_129 SS      129 26/05/…  2006     5     1 Pseudo… SS3      3.02  "M"  
##  5 P_herm_137 FRS     137 22/05/…  2006     5     1 Pseudo… FRSU     U.30  "M"  
##  6 P_herm_145 FRS     145 23/05/…  2006     5     1 Pseudo… FRSU     U.27  "M"  
##  7 P_herm_153 MC      153 28/05/…  2006     5     1 Pseudo… MC2      2.31  "M"  
##  8 P_herm_161 CS      161 20/09/…  2007     9    17 Pseudo… CS2G     2G.01 ""   
##  9 P_herm_169 CS      169 20/09/…  2007     9    17 Pseudo… CSMTG    MTG.… ""   
## 10 P_herm_177 CS      177 21/09/…  2007     9    17 Pseudo… CS2      2.03  ""   
## # ℹ 157 more rows
## # ℹ 7 more variables: id_site <fct>, comments <fct>, X <lgl>, lat <dbl>,
## #   lon <dbl>, datum <fct>, SiteName <chr>

Extract and plot site coordinates:

coords <- gen_meta %>%
  group_by(pop) %>%
  summarise(lon = mean(lon),
            lat = mean(lat),
            .groups = "drop") %>%
  sf::st_as_sf(coords = c("lon", "lat"), crs = 4326)

mapview(coords, label = coords$pop, map.types = "Esri.WorldImagery")

Looks like our sites cluster into roughly 3 spatial groups. For simplicity we can merge these sites into 3 subpopulations, which will make it a bit simpler for our simulation later.

coords_3 <- gen_meta %>%
  mutate(three_pop = case_when(pop %in% c("MC", "SS", "WS") ~ "BR",
                               pop %in% c("FRN", "FRS") ~ "BL",
                               pop %in% c("KSE", "CS") ~ "TR")) %>%
  group_by(three_pop) %>%
  summarise(lon = mean(lon),
         lat = mean(lat),
         .groups = "drop") %>%
  ungroup() %>%
  sf::st_as_sf(coords = c("lon", "lat"), crs = 4326)

mapview(coords_3, label = coords_3$three_pop, map.types = "Esri.WorldImagery")

As it happens these merged subpopulations are roughly equidistant, arranged in a triangle. If we assume the terrain between them is similarly easy to traverse we can simplify our model of the system even more and assume that all else being equal these subpopulations should have roughly similar migration rates between them. We called these subpopulations “BR”, “BL”, and “TR”, for Bottom Right, Bottom Left, and Top Right (creatively named I know).

Given this setup, let us have a look at the Fst values between these three subpopulations over the three years. We will use a custom function to calculate pairwise Fst for our subpopulations. We will do it this way so we can show some slimr functionality later, where we can exploit the similarity of R and SLiM code to run our Fst function from within SLiM, and get fast and live Fst estimates while a simulation is running. This means that the Fst calculated for our observed and our simulated data will be directly comparable when we do our downstream analysis.

sim.mutationFrequencies <- function(subpop) {
  gen <- get("gen", parent.frame(2))
  gl.alf(gen[pop = subpop])[ , 2]
}

isFinite <- function(x) is.finite(x)

calculateFST <- function(subpop1, subpop2) {
                  ## Calculate the FST between two subpopulations
                  p1_p = sim.mutationFrequencies(subpop1);
                  p2_p = sim.mutationFrequencies(subpop2);
                  mean_p = (p1_p + p2_p) / 2.0;
                  H_t = 2.0 * mean_p * (1.0 - mean_p);
                  H_s = p1_p * (1.0 - p1_p) + p2_p * (1.0 - p2_p);
                  fst = 1.0 - H_s/H_t;
                  fst = fst[isFinite(fst)]; ## exclude muts where mean_p is 0.0 or 1.0
                  return(mean(fst));
}

pairFST <- function(gen) {
  
  pops <- as.character(unique(pop(gen)))
  pop_pairs <- combn(pops, 2)
  pair_fst <- apply(pop_pairs, 2, function(x) calculateFST(x[1], x[2]))
  fst_mat <- matrix(nrow = length(pops), ncol = length(pops))
  rownames(fst_mat) <- colnames(fst_mat) <- pops
  fst_mat[t(pop_pairs)] <- pair_fst
  t(fst_mat)
  
}

We used several custom functions above and named them so that they would match available functions in SLiM. That way, when we run our simulations using slimr, SLiM will use its optimized versions of these functions internally. We also create a function to run the pairwise Fst function on all possible subpopulation pairs, which we will now run on our observed SNP data. We used a function in the dartR package to calculate allele frequencies to use in our Fst calculation. In SLiM, this is a builtin function, which we will take advantage of later. Note that the use of get and parent.frame is a trick to get R to use the right gen object without having to specify it as an argument, which will let us reuse the functions later in SLiM, which cannot access R objects while it is running.

gen_meta <- gen_meta %>%
  mutate(three_pop = case_when(pop %in% c("MC", "SS", "WS") ~ "BR",
                               pop %in% c("FRN", "FRS") ~ "BL",
                               pop %in% c("KSE", "CS") ~ "TR"))

gen@other$ind.metrics <- gen_meta 
pop(gen) <- gen_meta$three_pop

## remove population we have genetic data for but no abundance data
gen <- gen[gen@other$ind.metrics$pop != "WS"]


fst <- pairFST(gen)

fst
##            TR        BL BR
## TR         NA        NA NA
## BL 0.01437133        NA NA
## BR 0.01358429 0.0156385 NA

That is the overall Fst, but we really want to know how that is changing over our three sampling time points. We have three years of data, which happen to span a major rainfall event, from 2006, 2007, and 2008, where there was a major rainfall event in 2007. Let’s look at the pairwise Fst over the three years.

fst_by_year <- map(c(2006, 2007, 2008),
                   ~pairFST(gen[gen@other$ind.metrics$year == .x, ]) %>%
                     as.dist() %>%
                     as.matrix())
names(fst_by_year) <- c("2006", "2007", "2008")
fst_by_year
## $`2006`
##            TR         BR         BL
## TR 0.00000000 0.07423650 0.07492284
## BR 0.07423650 0.00000000 0.04449832
## BL 0.07492284 0.04449832 0.00000000
## 
## $`2007`
##            BL         TR         BR
## BL 0.00000000 0.02405952 0.03053511
## TR 0.02405952 0.00000000 0.02336933
## BR 0.03053511 0.02336933 0.00000000
## 
## $`2008`
##            BL         BR         TR
## BL 0.00000000 0.06184445 0.06037648
## BR 0.06184445 0.00000000 0.04472136
## TR 0.06037648 0.04472136 0.00000000

We can visualize that over the three years:

fst_df <- imap_dfr(fst_by_year,
                   ~combn(c("BL", "BR", "TR"), 2) %>%
                     t() %>%
                     as.data.frame() %>%
                     rename(pop1 = V1, pop2 = V2) %>%
                     mutate(fst = .x[cbind(pop1, pop2)],
                            year = .y,
                            pop_combo = paste(pop1, pop2, sep = " to ")))

ggplot(fst_df, aes(year, fst)) +
  geom_path(aes(colour = pop_combo, group = pop_combo)) +
  theme_minimal()

## save for later
#write_rds(fst_df, "data/fst_df.rds")

So we see that during the rainfall year a precipitous drop in Fst occurs, which has only partially recovered the following year. But perhaps before we proceed we should make sure this difference in Fst is really more than what we would expect based on random variation. To do this we will use a simple boostrapping approach.

## split populations by site and year to make it easier to bootstrap within
pop_year <- paste(pop(gen), gen@other$ind.metrics$year, sep = "_")
gen_set_bs <- gen 
pop(gen_set_bs) <- as.factor(pop_year)
## resample 15 individuals (with replacement) for each year site combination
gen_bs <- replicate(200, gl.sample(gen_set_bs, nsample = 15, verbose = 0), simplify = FALSE)
## run pairwise Fst code on each bootstrap sample
fst_bs <- map(gen_bs,
              ~ map(c("2006", "2007", "2008"),
                   function(year) pairFST(.x[grep(year, as.character(pop(.x))), ]) %>%
                     as.dist() %>%
                     as.matrix()) %>%
                setNames(c("2006", "2007", "2008")))
## convert to tibble
fst_bs_df <- lapply(fst_bs, function(x) imap_dfr(x,
                                                 ~ combn(c("BL", "BR", "TR"), 2) %>%
                                                   t() %>%
                                                   as.data.frame() %>%
                                                   rename(pop1 = V1, pop2 = V2) %>%
                                                   mutate(pop_1 = paste(pop1, .y, sep = "_"),
                                                          pop_2 = paste(pop2, .y, sep = "_")) %>%
                                                   mutate(fst = .x[cbind(pop_1, pop_2)],
                                                          year = .y,
                                                          pop_combo = paste(pop1, pop2, sep = " to ")))) %>%
  imap(~ .x %>% mutate(rep = .y)) %>%
  list_rbind()
## create fst ci
fst_ci <- fst_bs_df %>%
  group_by(year, pop_combo) %>%
  summarise(mean_fst = mean(fst),
            ci_lower = quantile(fst, 0.025),
            median_fst = quantile(fst, 0.5),
            ci_upper = quantile(fst, 0.975))
## `summarise()` has grouped output by 'year'. You can override using the
## `.groups` argument.
## test year diffs
fst_years_diffs <- fst_bs_df %>%
  group_by(year, rep) %>%
  summarise(fst = mean(fst)) %>%
  group_by(rep) %>%
  summarise(year_07_06 = fst[year == "2006"] - fst[year == "2007"],
            year_08_06 = fst[year == "2008"] - fst[year == "2006"],
            year_07_08 = fst[year == "2008"] - fst[year == "2007"]) %>%
  summarise(year_07_06_mean = mean(year_07_06),
            year_07_06_lower = quantile(year_07_06, 0.025),
            year_07_06_median = quantile(year_07_06, 0.5),
            year_07_06_upper = quantile(year_07_06, 0.975),
            year_08_06_mean = mean(year_08_06),
            year_08_06_lower = quantile(year_08_06, 0.025),
            year_08_06_median = quantile(year_08_06, 0.5),
            year_08_06_upper = quantile(year_08_06, 0.975),
            year_07_08_mean = mean(year_07_08),
            year_07_08_lower = quantile(year_07_08, 0.025),
            year_07_08_median = quantile(year_07_08, 0.5),
            year_07_08_upper = quantile(year_07_08, 0.975))
## `summarise()` has grouped output by 'year'. You can override using the
## `.groups` argument.

Here is the same plot as above but with bootstrapped errorbars with 95% confidence intervals.

ggplot(fst_ci, aes(year, mean_fst)) +
  geom_path(aes(colour = pop_combo, group = pop_combo)) +
  geom_point(aes(colour = pop_combo, group = pop_combo)) +
  geom_errorbar(aes(colour = pop_combo, group = pop_combo,
                    ymin = ci_lower, ymax = ci_upper),
                width = 0.1) +
  theme_minimal()

It is hard to tell whether these differences are meaningful, but the fst_years_diffs has bootstrapped estimates of the mean difference between years. Any confidence interval that does not overlap zero consitutes evidence that there is a real difference between those years.

year_diff_df <- fst_years_diffs %>%
  pivot_longer(everything(), names_to = "label", values_to = "fst_diff") %>%
  separate(label, c("chaff", "year_1", "year_2", "quant"), "_") %>%
  mutate(year_1 = paste0("20", year_1),
         year_2 = paste0("20", year_2)) %>%
  select(-chaff) %>%
  pivot_wider(names_from = quant, values_from = fst_diff) %>%
  mutate(`Fst Difference` = paste0(year_1, " - ", year_2))

ggplot(year_diff_df, aes(mean, `Fst Difference`)) +
  geom_point() +
  geom_linerange(aes(xmin = lower,
                     xmax = upper)) +
  geom_vline(xintercept = 0, linetype = 2) +
  theme_minimal()

This suggests that indeed, on average, the populations in 2006 and 2008 were significantly higher in Fst than in the intervening year of 2007, but Fst was more or less equal when comparing 2008 to 2006. So a significant drop in Fst followed by a full recovery the next year.

Now, can we think of a reason why this pattern might occur? We have a hypothesis! First, we’ll lay it out, then we will interrogate its logic using the simulation tools provided by slimr. Earlier on we talked about how the desert “turns green” after a big rainfall event. This implies it reverts back to dry or “red” sands afterwards. Generally this is true, however, some patches remain relatively green during the dry periods. These are sometimes know as mesic ‘refugia’. We hypothesized that during dry periods, our mouse population recedes into these separate refugia across the landscape, where they become relatively reproductively isolated. This leads to an inexorable increase in Fst due to a genetic drift without gene flow. Then, when the rain comes, the whole landscape becomes wet, the mice come out to play, spreading throughout, reproducing along the way. The subpopulations of the refugia intermix, leading to an erasure of the genomic differentiation that had been in progress during the dry years. But can this mechanism lead to the level of observed change in such a short period? Are there other possible mechanisms? Let’s explore these ideas with simulation!

A simple simulation

To begin setup a simple simulation that captures some of the features of the system we are studying. We will simulate three subpopulations, as we have in our data, and allow migration between them at equal rates. We will include evolutionary processes such as mutation, selection, sexual reproduction, and recombination as additional processes happening in our simulation. Our initial question is simply, can we observe Fst values that change through time in a manner consistent with our observed changes using combinations of just these processes? Thanks to slimr, we can continue and write out the logic of our proposed simulation directly in R, using syntax very similar to SLiM (>3.0). We can additionally make the simulation code easy to update with different parameter combinations to try out.

We will use the following parameters in our simulation:

Parameter Description
mut_rate mutation rate of simulated population
genome_size size of simulated genome
selection_strength mean strength of selection specified as a standard deviation of selection coeffs
migration_rates rate of migration amongst three pops when abundance is high, must be between 0 and 1
abund_threshold abundance (before scaling) above which migration between populations is “turned” on
recomb_rate recombination rate, a nuisance parameter
popsize_scaling multiply observed abundances by this value to get total subpop size

It should be noted that the following simulation are for SLiM version 4.0 or greater. They should also work in SLiM <4.0 by changing any references to sim.cycle to sim.generation.

Now for the code:

default_genome_size <- 300000

pop_sim <- slim_script(
  
  slim_block(initialize(), {
    
    setSeed(12345);
    initializeMutationRate(slimr_template("mut_rate", 1e-6));
    initializeMutationType("m1", 0.5, "n", 0, slimr_template("selection_strength", 0.1));
    initializeGenomicElementType("g1", m1, 1.0);
    initializeGenomicElement(g1, 0, slimr_template("genome_size", default_genome_size) - 1);
    initializeRecombinationRate(slimr_template("recomb_rate", 1e-8));
    initializeSex("A");
    defineConstant("abund", slimr_inline(pop_abunds, delay = TRUE));
    defineConstant("sample_these", slimr_inline(sample_these, delay = TRUE));
    
  }),
  slim_block(1, {
    
    init_pop = slimr_inline(init_popsize, delay = TRUE)
    
    ## set populations to initial size
    sim.addSubpop("p1", asInteger(init_pop[0]));
    sim.addSubpop("p2", asInteger(init_pop[1]));
    sim.addSubpop("p3", asInteger(init_pop[2]));
    
  }),
  
  slim_block(1, late(), {
    ## get starting population from a file which we will fill-in later
    sim.readFromPopulationFile(slimr_inline(starting_pop, delay = TRUE));
    ## migration on or off flags for pops 1-3 (using tag)
    p1.tag = 0;
    p2.tag = 0;
    p3.tag = 0;
  }),
  
  slim_block(1, 1000, late(), {
    
    ## update generation number
    ## note in SLiM versions < 4.0, this would be sim.generation instead
    gen = sim.cycle %% 50
    if(gen == 0) {
      gen = 50
    }
    
    ## set population size to observed levels
    p1.setSubpopulationSize(asInteger(ceil(abund[0, gen - 1] * slimr_template("popsize_scaling", 100))));
    p2.setSubpopulationSize(asInteger(ceil(abund[1, gen - 1] * ..popsize_scaling..)));
    p3.setSubpopulationSize(asInteger(ceil(abund[2, gen - 1] * ..popsize_scaling..)));
    
    ## increase migration when above abundance threshold
    if(p1.tag == 0 & abund[0, gen - 1] > slimr_template("abund_threshold", 5)) {
      p2.setMigrationRates(p1, slimr_template("migration_rate", 0))
      p3.setMigrationRates(p1, ..migration_rate..)
      p1.tag = 1;
    } 
    if(p1.tag == 1 & abund[0, gen - 1] <= ..abund_threshold..) {
      p2.setMigrationRates(p1, 0)
      p3.setMigrationRates(p1, 0)
      p1.tag = 0;
    }
    
    if(p2.tag == 0 & abund[1, gen - 1] > ..abund_threshold..) {
      p1.setMigrationRates(p2, ..migration_rate..)
      p3.setMigrationRates(p2, ..migration_rate..)
      p2.tag = 1;
    } 
    if(p2.tag == 1 & abund[1, gen - 1] <= ..abund_threshold..) {
      p1.setMigrationRates(p2, 0)
      p3.setMigrationRates(p2, 0)
      p2.tag = 0;
    }    
    
    if(p3.tag == 0 & abund[2, gen - 1] > ..abund_threshold..) {
      p1.setMigrationRates(p3, ..migration_rate..)
      p2.setMigrationRates(p3, ..migration_rate..)
      p3.tag = 1;
    } 
    if(p3.tag == 1 & abund[2, gen - 1] <= ..abund_threshold..) {
      p1.setMigrationRates(p3, 0)
      p2.setMigrationRates(p3, 0)
      p3.tag = 0;
    }
    
  }),
  
  slim_block(1000, late(), {
    slimr_output_full()
  })
  
)

pop_sim
## <slimr_script[5]>
## block_init:initialize() {
##     setSeed(12345);
##     initializeMutationRate(..mut_rate..);
##     initializeMutationType("m1", 0.5, "n", 0, ..selection_strength..);
##     initializeGenomicElementType("g1", m1, 1);
##     initializeGenomicElement(g1, 0, ..genome_size.. - 1);
##     initializeRecombinationRate(..recomb_rate..);
##     initializeSex("A");
##     defineConstant("abund", r_inline(pop_abunds));
##     defineConstant("sample_these", r_inline(sample_these));
## }
## 
## block_2:1 early() {
##     init_pop = r_inline(init_popsize);
##     sim.addSubpop("p1", asInteger(init_pop[0]));
##     sim.addSubpop("p2", asInteger(init_pop[1]));
##     sim.addSubpop("p3", asInteger(init_pop[2]));
## }
## 
## block_3:1 late() {
##     sim.readFromPopulationFile(r_inline(starting_pop));
##     p1.tag = 0;
##     p2.tag = 0;
##     p3.tag = 0;
## }
## 
## block_4:1:1000 late() {
##     gen = sim.cycle%%50;
##     if (gen == 0) {
##         gen = 50;
##     }
##     p1.setSubpopulationSize(asInteger(ceil(abund[0, gen - 1] * ..popsize_scaling..)));
##     p2.setSubpopulationSize(asInteger(ceil(abund[1, gen - 1] * ..popsize_scaling..)));
##     p3.setSubpopulationSize(asInteger(ceil(abund[2, gen - 1] * ..popsize_scaling..)));
##     if (p1.tag == 0 & abund[0, gen - 1] > ..abund_threshold..) {
##         p2.setMigrationRates(p1, ..migration_rate..);
##         p3.setMigrationRates(p1, ..migration_rate..);
##         p1.tag = 1;
##     }
##     if (p1.tag == 1 & abund[0, gen - 1] <= ..abund_threshold..) {
##         p2.setMigrationRates(p1, 0);
##         p3.setMigrationRates(p1, 0);
##         p1.tag = 0;
##     }
##     if (p2.tag == 0 & abund[1, gen - 1] > ..abund_threshold..) {
##         p1.setMigrationRates(p2, ..migration_rate..);
##         p3.setMigrationRates(p2, ..migration_rate..);
##         p2.tag = 1;
##     }
##     if (p2.tag == 1 & abund[1, gen - 1] <= ..abund_threshold..) {
##         p1.setMigrationRates(p2, 0);
##         p3.setMigrationRates(p2, 0);
##         p2.tag = 0;
##     }
##     if (p3.tag == 0 & abund[2, gen - 1] > ..abund_threshold..) {
##         p1.setMigrationRates(p3, ..migration_rate..);
##         p2.setMigrationRates(p3, ..migration_rate..);
##         p3.tag = 1;
##     }
##     if (p3.tag == 1 & abund[2, gen - 1] <= ..abund_threshold..) {
##         p1.setMigrationRates(p3, 0);
##         p2.setMigrationRates(p3, 0);
##         p3.tag = 0;
##     }
## }
## 
## block_5:1000 late() {
##     {sim.outputFull() -> full_output}
## }
## This slimr_script has templating in block(s) block_init
## and block_4 for variables mut_rate and
## selection_strength and genome_size and recomb_rate and
## popsize_scaling and abund_threshold and migration_rate.

The above script uses a number of different slimr feature that we will explain here. To anyone who has used SLiM before, the above script probably looks pretty familiar. For those of you not familiar with SLiM, we will go over each part of the above script one by one to gain an understanding of what the script is doing. Otherwise, if you’d like to know more about SLiM, I would highly recommend reading the SLiM manual, which is very detailed and packed full of examples.

We start by setting a variable that we will use in the slimr_script and later on in the R script as well.

We then use the slimr_script() function to create a new slimr_script object, which will contain all the information needed to run it in SLiM later. slimr_script takes a list of slimr_block objects as arguments, which are created with the slimr_block() function. slimr_block objects contain blocks of SLiM code. Generally, there are two main types of code blocks in SLiM: initialize blocks and ‘event’ blocks. Out first block above is an initialize block, which sets up the SLiM simulation. All slimr_script() calls must contain at least one initialize block. Most of the action with slimr happens in this block for this script. In it, we set up the simulation with our parameters, using two ‘slimr verbs’. ‘slimr verbs’ are R functions that can be inserted into SLiM code and which modify the code for various useful purposes. Here we are using slimr_template(), which lets you created ‘templated’ variables in a script – these are placemarkers that can be filled in with desired values later. We also use slimr_inline(), which let’s you ‘inline’ R objects directly into a script. Inlining means the object and its values are converted into a text representation and passed to SLiM by embedding them directly into the script. This all happens behind the scenes, so you don’t need to worry about it if you are not interested. We have used the special argument delay = TRUE to delay the evaluation of slimr_inline() until later. This is because we haven’t created the R objects slimr_inline() references yet.

The initialize block is followed by 4 more slimr_block calls, each of which specifies SLiM ‘events’. “block_2” sets up 3 subpopulations in generation 1, and “block_3” sets up starting conditions for these subpopulation by reading from a file (which we create later), also in generation 1. “block_4” contains most of the logic of the simulation, running in every generation from 1 to 1000. Finally, “block_5” outputs the state of the simulation at the last generation, generation 1000. We assigned the slimr_script object to a variable called pop_sim, and printed this object, obtaining a pretty output of our script, including special syntax showing our templated variable, which look like this: ..variable_name.. So, in order to run this script in SLiM, we first need to fill in values for our templated variables, and also make sure we the R objects referred to in slimr_inline() calls exist in our R session. We can then “render” our script into a runnable form.

so, how do we fill in the templated variables with values and fill-in our slimr_inline calls with actual R objects? For that, we use the slim_script_render function. The first thing we need to do before we use it is to create the R objects that slimr_inline refers to, otherwise we will get an error about non-existent objects. So what do we need? We need a matrix of abundances for our three subpopulations (for slimr_inline(pop_abunds)), a matrix of starting population abundances (for slimr_inline(init_popsize)) and a file name pointing to a SLiM population data file containing initial population conditions (for slimr_inline(starting_pop). We will later create this file from our genlight object using slimr. But first, let’s get our objects in order, choose some parameter values and render ourselves a slimr_script!

For our population abundances over time, we will use the live-trapping data, and we will create a simplified cycle, taking only part of the sequence, and then repeating it over and over throughout the years. We will sample data between 1995 and 2009, interpolate that to 50 time points, then loop over it inside our slimr_script, as we discussed above.

pop_abunds <- abund_summ %>%
  filter(date < "2009-10-01" & date > "1995-03-01") %>%
  mutate(three_pop = case_when(pop %in% c("MC", "SS", "WS") ~ "BR",
                               pop %in% c("FRN", "FRS") ~ "BL",
                               pop %in% c("KSE", "CS") ~ "TR")) %>%
  drop_na(three_pop) %>%
  group_by(date, three_pop) %>%
  summarise(abund = mean(abund, na.rm = TRUE),
            .groups = "drop") 

pop_abunds <- pop_abunds %>%
  pivot_wider(names_from = date, values_from = abund) %>%
  as.matrix()

pop_abunds <- rbind(approx(pop_abunds[1, ], n = 50)$y,
                    approx(pop_abunds[2, ], n = 50)$y,
                    approx(pop_abunds[3, ], n = 50)$y)
## Warning in xy.coords(x, y, setLab = FALSE): NAs introduced by coercion

## Warning in xy.coords(x, y, setLab = FALSE): NAs introduced by coercion

## Warning in xy.coords(x, y, setLab = FALSE): NAs introduced by coercion
## replace exact zeroes
pop_abunds[pop_abunds == 0] <- 0.02

## set sample times corresponding to our genetic data (roughly)
sample_times <- c("2006" = 40, "2007" = 45, "2008" = 49)

## plot our abundance sequence

plot(pop_abunds[2, ], type = "l", col = "blue")
lines(pop_abunds[1, ], col = "red")
lines(pop_abunds[3, ], col = "green")
abline(v = sample_times)

## save for later use
#write_rds(pop_abunds, "data/pop_abunds.rds")

So, that is out pop_abunds. For initial population states we are going to base it on data from 2008, which is the third sampling date, and which corresponds to what we think is a panmictic population, which is just after, but not during the big rainfall event. We will get the initial population sizes from here, as well as our starting population data. Our initial population sizes need to be based on this (rather than our abundance data we just generated above), because the number of individuals at the start of our simulation must match the number in the starting population data. The population will then be immediately adjusted to our desired population size by essentially drawing offspring from our smaller starting population pool. This should give us populations with SNP frequencies resembling our actual data at the start of the simulation.

## extract 2008 data
gen_2008 <- gen[gen@other$ind.metrics$year == 2008, ]
## count number of individuals in genetic sample per subpopulation
init_popsize <- c(table(pop(gen_2008)))
## set filename to be used for starting pop data
starting_pop = tempfile(fileext = ".txt") 
## setup generations to sample
## we will just sample the three years corresponding to the data, but do it during the last 6 cycles, as "technical" replicates
sample_these <- purrr::map(c(14:19),
                           ~50*.x + sample_times) %>%
  purrr::reduce(union)

## save for later
#write_rds(sample_these, "data/sample_these.rds")
#write_rds(init_popsize, "data/init_popsize.rds")

Okay, now we are ready to use slim_script_render to generate a script we can run. The first thing we can try is to render the script without providing a template. Since we have provided defaults for all of our templated variables, this should generate a script with the default values (our ‘default’ script).

script_def <- slim_script_render(pop_sim)
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
script_def
## <slimr_script[5]>
## block_init:initialize() {
##     setSeed(12345);
##     initializeMutationRate(1e-06);
##     initializeMutationType("m1", 0.5, "n", 0, 0.1);
##     initializeGenomicElementType("g1", m1, 1);
##     initializeGenomicElement(g1, 0, 3e+05 - 1);
##     initializeRecombinationRate(1e-08);
##     initializeSex("A");
##     defineConstant("abund", {. = matrix(c(0.926, 0.278, 0.463, 0.557, 0.153, ...), nrow = 3, ncol = 50)});
##     defineConstant("sample_these", {. = c(740, 745, 749, 790, 795, 799, 840, 845, 849, 890, ...)});
## }
## 
## block_2:1 early() {
##     init_pop = {. = c(7, 13, 13)}
##     sim.addSubpop("p1", asInteger(init_pop[0]));
##     sim.addSubpop("p2", asInteger(init_pop[1]));
##     sim.addSubpop("p3", asInteger(init_pop[2]));
## }
## 
## block_3:1 late() {
##     sim.readFromPopulationFile({. = "F:\\Rtemp\\RtmpaQoHBg\\file5b0c1e3b74c1.txt"});
##     p1.tag = 0;
##     p2.tag = 0;
##     p3.tag = 0;
## }
## 
## block_4:1:1000 late() {
##     gen = sim.cycle%%50;
##     if (gen == 0) {
##         gen = 50;
##     }
##     p1.setSubpopulationSize(asInteger(ceil(abund[0, gen - 1] * 100)));
##     p2.setSubpopulationSize(asInteger(ceil(abund[1, gen - 1] * 100)));
##     p3.setSubpopulationSize(asInteger(ceil(abund[2, gen - 1] * 100)));
##     if (p1.tag == 0 & abund[0, gen - 1] > 5) {
##         p2.setMigrationRates(p1, 0);
##         p3.setMigrationRates(p1, 0);
##         p1.tag = 1;
##     }
##     if (p1.tag == 1 & abund[0, gen - 1] <= 5) {
##         p2.setMigrationRates(p1, 0);
##         p3.setMigrationRates(p1, 0);
##         p1.tag = 0;
##     }
##     if (p2.tag == 0 & abund[1, gen - 1] > 5) {
##         p1.setMigrationRates(p2, 0);
##         p3.setMigrationRates(p2, 0);
##         p2.tag = 1;
##     }
##     if (p2.tag == 1 & abund[1, gen - 1] <= 5) {
##         p1.setMigrationRates(p2, 0);
##         p3.setMigrationRates(p2, 0);
##         p2.tag = 0;
##     }
##     if (p3.tag == 0 & abund[2, gen - 1] > 5) {
##         p1.setMigrationRates(p3, 0);
##         p2.setMigrationRates(p3, 0);
##         p3.tag = 1;
##     }
##     if (p3.tag == 1 & abund[2, gen - 1] <= 5) {
##         p1.setMigrationRates(p3, 0);
##         p2.setMigrationRates(p3, 0);
##         p3.tag = 0;
##     }
## }
## 
## block_5:1000 late() {
##     {sim.outputFull() -> full_output}
## }

Okay, so now we can see that we have an indication that our R objects will be inserted into the SLiM script. We can try and run this in SLiM now, but we will get an error:

test <- slim_run(script_def)
## 
## 
## Simulation finished with exit status: 1
## Warning: 
## Failed! Error:
## ERROR (Species::InitializePopulationFromFile): initialization file does not exist or is empty.
## 
## Error on script line 26, character 8:
## 
##     sim.readFromPopulationFile("F:\\Rtemp\\RtmpaQoHBg\\file5b0c1e3b74c1.txt");
##         ^^^^^^^^^^^^^^^^^^^^^^

This is because the starting_pop.txt doesn’t exist yet, and SLiM has thrown an error telling us this (which slimr passes from SLiM to us through the R console). Let’s create the initial population file using slim_make_pop_init. We need to pass this function either a genlight or a SNP matrix. Since our genlight object has some missing values, and SLiM cannot handle missing values, we will first convert to a SNP matrix, and then fill-in missing values by interpolation. Essentially we just fill-in missing values randomly with a draw from a binomial distribution.

snp_mat <- as.matrix(gen_2008)

glimpse(snp_mat)
##  int [1:33, 1:39978] 0 0 0 0 0 0 0 0 0 0 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : chr [1:33] "P_herm_281" "P_herm_289" "P_herm_297" "P_herm_265" ...
##   ..$ : chr [1:39978] "15067413-48-G/A" "15067414-5-C/T" "15067416-63-G/A" "15067419-10-G/C" ...
## replace NAs
nas <- apply(snp_mat, 2, function(x) !any(!is.finite(x)))
snp_mat[ , !nas] <- apply(snp_mat[ , !nas], 2, function(x) {
  tab <- table(x[is.finite(x)]);
  x[!is.finite(x)] <- as.integer(sample(names(tab), sum(!is.finite(x)), replace = TRUE, prob = tab / sum(tab)));
  x
})

glimpse(snp_mat)
##  int [1:33, 1:39978] 0 0 0 0 0 0 0 0 0 0 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : chr [1:33] "P_herm_281" "P_herm_289" "P_herm_297" "P_herm_265" ...
##   ..$ : chr [1:39978] "15067413-48-G/A" "15067414-5-C/T" "15067416-63-G/A" "15067419-10-G/C" ...

Then, we make our starting population file!

sexes <- as.character(gen_2008@other$ind.metrics$Sex)

## sex ratio of sample is skewed to female, so replace actually sexes with extra males
## otherwise simulation crashes because it is unable to sample enough females? Need
## to look more into this bug.
sexes[pop(gen_2008) == "BL"] <- c("M", "F")
## Warning in sexes[pop(gen_2008) == "BL"] <- c("M", "F"): number of items to
## replace is not a multiple of replacement length
sexes[pop(gen_2008) == "BR"] <- c("M", "F")
## Warning in sexes[pop(gen_2008) == "BR"] <- c("M", "F"): number of items to
## replace is not a multiple of replacement length
sexes[pop(gen_2008) == "TR"] <- c("M", "F")
## Warning in sexes[pop(gen_2008) == "TR"] <- c("M", "F"): number of items to
## replace is not a multiple of replacement length
## now make initial population file
slim_make_pop_input(snp_mat, starting_pop, ## filename 
                    sim_gen = 1, ## set generation to first generation
                    ind_pops = gen_2008@other$ind.metrics$three_pop, ## use subpops
                    ind_sex = sexes, ## set sexes
                    ## random positions:
                    mut_pos = sample.int(default_genome_size - 1, nLoc(gen_2008)),
                    version = 3)
## [1] "F:\\Rtemp\\RtmpaQoHBg\\file5b0c1e3b74c1.txt"
## look at the first 50 line to see if it worked:
read_lines(starting_pop) %>%
  head(50)
##  [1] "#OUT 1 A"                      "Version: 3"                   
##  [3] "Populations:"                  "p1 7 S 0.42857142857142855"   
##  [5] "p2 13 S 0.46153846153846156"   "p3 13 S 0.46153846153846156"  
##  [7] "Mutations:"                    "0 0 m1 117793 0 0.5 p1 1 2"   
##  [9] "1 1 m1 166306 0 0.5 p1 1 9"    "2 2 m1 230205 0 0.5 p1 1 4"   
## [11] "3 3 m1 9509 0 0.5 p1 1 14"     "4 4 m1 47183 0 0.5 p1 1 8"    
## [13] "5 5 m1 42660 0 0.5 p1 1 4"     "6 6 m1 179506 0 0.5 p1 1 4"   
## [15] "7 7 m1 202318 0 0.5 p1 1 5"    "8 8 m1 84153 0 0.5 p1 1 2"    
## [17] "9 9 m1 108690 0 0.5 p1 1 3"    "10 10 m1 160149 0 0.5 p1 1 6" 
## [19] "11 11 m1 161502 0 0.5 p1 1 16" "12 12 m1 122621 0 0.5 p1 1 25"
## [21] "13 13 m1 22539 0 0.5 p1 1 2"   "14 14 m1 131828 0 0.5 p1 1 1" 
## [23] "15 15 m1 256203 0 0.5 p1 1 66" "16 16 m1 241478 0 0.5 p1 1 3" 
## [25] "17 17 m1 206033 0 0.5 p1 1 66" "18 18 m1 14002 0 0.5 p1 1 1"  
## [27] "19 19 m1 164031 0 0.5 p1 1 62" "20 20 m1 41191 0 0.5 p1 1 12" 
## [29] "21 21 m1 8677 0 0.5 p1 1 15"   "22 22 m1 94041 0 0.5 p1 1 64" 
## [31] "23 23 m1 148047 0 0.5 p1 1 8"  "24 24 m1 96155 0 0.5 p1 1 2"  
## [33] "25 25 m1 25365 0 0.5 p1 1 3"   "26 26 m1 196396 0 0.5 p1 1 14"
## [35] "27 27 m1 100302 0 0.5 p1 1 18" "28 28 m1 209291 0 0.5 p1 1 1" 
## [37] "29 29 m1 207284 0 0.5 p1 1 2"  "30 30 m1 6485 0 0.5 p1 1 1"   
## [39] "31 31 m1 178114 0 0.5 p1 1 8"  "32 32 m1 186245 0 0.5 p1 1 3" 
## [41] "33 33 m1 174584 0 0.5 p1 1 65" "34 34 m1 66593 0 0.5 p1 1 1"  
## [43] "35 35 m1 150064 0 0.5 p1 1 1"  "36 36 m1 163884 0 0.5 p1 1 19"
## [45] "37 37 m1 257974 0 0.5 p1 1 11" "38 38 m1 287205 0 0.5 p1 1 18"
## [47] "39 39 m1 130973 0 0.5 p1 1 4"  "40 40 m1 216234 0 0.5 p1 1 63"
## [49] "41 41 m1 280741 0 0.5 p1 1 11" "42 42 m1 106279 0 0.5 p1 1 8"

And now we can try running our simulation:

res <- slim_run(script_def, throw_error = TRUE)
## 
## 
## Simulation finished with exit status: 0
## 
## Success!

Okay, so now we have a working simulation! We’ve currently just output the full population information at the end of the simulation, which doesn’t give us too much information about our question, but just to convince ourselves the simulation produced genetic information, we can extract our output as a genlight object and plot it. Note: The conversion can take awhile.

pop_res <- slimr::slim_extract_genlight(res, "full_output")
## reorder by subpop
pop_res <- pop_res[order(pop(pop_res)), ]
plot(pop_res)

So this shows that our three populations are characterized by mostly non-overlapping fixed mutations. This makes sense since our default parameters specified some selection, and no migration. Mutation with positive selection coefficients would have sweeped to fixation, and the lack of migration between subpopulation means that each subpopulation is likely to have had a different set of mutations go to fixation, and no way for these to then spread to other subpopulations.

Okay, to get an idea of what sort of dynamics of Fst we will get with different versions of this simulation, let’s create a version where we calculate Fst during the simulation, and output it for tracking. Then we can make some time-series plots. When it comes to comparing simulation outputs to our actual data we will make yet another version where we will only output just before, during, and just after the population explosions corresponding to rainfall. To add a Fst calculation to our simulation we will use the slim_function function, which allows you to specify a SLiM function to use in your SLiM code. This Fst function is based on the one described in the SLiM manual, and can also be found online here: https://github.com/MesserLab/SLiM-Extras/blob/master/functions/calcFST.txt. We have already used the function as an R function when we calculated Fst for our observed data. Now we can convert it to a SLiM function. slim_function requires us to specify the function arguments and the function names as character strings, and then the body of the function as slimr code. If inserted into a slim_script call, the function will then be accessible inside the rest of the code in the slim_script call. We can use the previously specified R function code body by simply extracting it using rlang::fn_body(). We also need to make sure it is evaluated before insertion, which we assure using the evaluation forcing operator !!.

Note: The latest version of SLiM (v4.1), which was released after the initial version of this vignette was written, now has a built-in function called calcFST, which means we technically no longer need to use a custom function for this functionality. However, we have kept this section of the vignette to show how custom functions can be specified and used in slimr.

slim_script(
  
  slim_function("o<Subpopulation>$ subpop1", "o<Subpopulation>$ subpop2",
                name = "calculateFST",
                return_type = "f$", body = calculateFST),
  
  slim_block(initialize(), {
    
    setSeed(12345);
    initializeMutationRate(slimr_template("mut_rate", 1e-6));
    initializeMutationType("m1", 0.5, "n", 0, slimr_template("selection_strength", 0.1));
    initializeGenomicElementType("g1", m1, 1.0);
    initializeGenomicElement(g1, 0, slimr_template("genome_size", !!default_genome_size) - 1);
    initializeRecombinationRate(slimr_template("recomb_rate", 1e-8));
    initializeSex("A");
    defineConstant("abund", slimr_inline(pop_abunds, delay = TRUE));
    defineConstant("sample_these", slimr_inline(sample_these, delay = TRUE));
    
  }),
  slim_block(1, {
    
    init_pop = slimr_inline(init_popsize, delay = TRUE)
    
    ## set populations to initial size
    sim.addSubpop("p1", asInteger(init_pop[0]));
    sim.addSubpop("p2", asInteger(init_pop[1]));
    sim.addSubpop("p3", asInteger(init_pop[2]));
    
  }),
  
  slim_block(1, late(), {
    ## get starting population from a file which we will fill-in later
    sim.readFromPopulationFile(slimr_inline(starting_pop, delay = TRUE));
    ## migration on or off flags for pops 1-3 (using tag)
    p1.tag = 0;
    p2.tag = 0;
    p3.tag = 0;
  }),
  
  slim_block(1, 1000, late(), {
    
    ## update generation number
    ## note in SLiM versions < 4.0, this would be sim.generation instead
    gen = sim.cycle %% 50
    if(gen == 0) {
      gen = 50
    }
    
    ## set population size to observed levels
    p1.setSubpopulationSize(asInteger(ceil(abund[0, gen - 1] * slimr_template("popsize_scaling", 100))));
    p2.setSubpopulationSize(asInteger(ceil(abund[1, gen - 1] * ..popsize_scaling..)));
    p3.setSubpopulationSize(asInteger(ceil(abund[2, gen - 1] * ..popsize_scaling..)));
    
    ## increase migration when above abundance threshold
    if(p1.tag == 0 & abund[0, gen - 1] > slimr_template("abund_threshold", 5)) {
      p2.setMigrationRates(p1, slimr_template("migration_rate", 0))
      p3.setMigrationRates(p1, ..migration_rate..)
      p1.tag = 1;
    } 
    if(p1.tag == 1 & abund[0, gen - 1] <= ..abund_threshold..) {
      p2.setMigrationRates(p1, 0)
      p3.setMigrationRates(p1, 0)
      p1.tag = 0;
    }
    
    if(p2.tag == 0 & abund[1, gen - 1] > ..abund_threshold..) {
      p1.setMigrationRates(p2, ..migration_rate..)
      p3.setMigrationRates(p2, ..migration_rate..)
      p2.tag = 1;
    } 
    if(p2.tag == 1 & abund[1, gen - 1] <= ..abund_threshold..) {
      p1.setMigrationRates(p2, 0)
      p3.setMigrationRates(p2, 0)
      p2.tag = 0;
    }    
    
    if(p3.tag == 0 & abund[2, gen - 1] > ..abund_threshold..) {
      p1.setMigrationRates(p3, ..migration_rate..)
      p2.setMigrationRates(p3, ..migration_rate..)
      p3.tag = 1;
    } 
    if(p3.tag == 1 & abund[2, gen - 1] <= ..abund_threshold..) {
      p1.setMigrationRates(p3, 0)
      p2.setMigrationRates(p3, 0)
      p3.tag = 0;
    }
    
    ## output Fst
    slimr_output(c(calculateFST(p1, p2), calculateFST(p1, p3), calculateFST(p2, p3)), "fsts");
    
  }),
  
  slim_block(1000, late(), {
    sim.simulationFinished()
  })
  
) -> pop_sim_fst

Now let’s run that for our defaults to see how it works.

fst_script <- slim_script_render(pop_sim_fst)
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
fst_res <- slim_run(fst_script, throw_error = TRUE)
## 
## 
## Simulation finished with exit status: 0
## 
## Success!

So now we can have a look at how the Fst data was returned, which is the default way slimr returns data that is a vector:

fst_res$output_data
## # A tibble: 1,000 × 5
##    generation name  expression                                       type  data 
##         <int> <chr> <chr>                                            <chr> <chr>
##  1          1 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.04…
##  2          2 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.04…
##  3          3 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.05…
##  4          4 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.05…
##  5          5 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.06…
##  6          6 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.09…
##  7          7 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.10…
##  8          8 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.11…
##  9          9 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.11…
## 10         10 fsts  c(calculateFST(p1, p2), calculateFST(p1, p3), c… "flo… 0.11…
## # ℹ 990 more rows
typeof(fst_res$output_data$data)
## [1] "character"

So it is currently in the form of a character vector separated by spaces. This is pretty straightforward to extract, so let’s do it (Note: slimr has the ability to automatically convert this output to more user friendly form, but here we will do it the hard way to show how you can flexibly extract different kinds of data, even if a built-in slimr function doesn’t exist to handle the type of output you want).

extract_fst <- function(fst_res) {
  fst_dat <- fst_res$output_data %>%
        dplyr::select(generation, data) %>%
        dplyr::mutate(data = str_split(data, " ")) %>%
        tidyr::unnest_longer(data, values_to = "fst", indices_to = "index") %>%
        dplyr::mutate(fst = as.numeric(fst), `subpop\npair` = c("BL-BR", "BL-TR", "BR-TR")[index]) 
  fst_dat
}

fst_dat <- extract_fst(fst_res)

ggplot(fst_dat, aes(generation, fst)) +
  geom_path(aes(colour = `subpop\npair`)) +
  scale_y_sqrt() +
  ggforce::facet_zoom(x = generation > 750 & generation < 1000,
                      y = fst > 0.1,
                      horizontal = FALSE,
                      zoom.size = 1)

We can see that pairwise Fst rapidly increases between all three subpopulations, reaching nearly its theoretical maximum of 1.0, and then periodically dipping slightly. Let’s look at how this equilibrium pattern corresponds with population sizes. We will look at mean population size and mean Fst.

pop_mean <- apply(pop_abunds, 2, mean) %>%
  rep(length.out = n_distinct(fst_dat$generation)) * 100 ## don't forget popsize_scaling

plot_cycle <- function(fst_dat, pop_mean) {
  fst_pop <- fst_dat %>%
    dplyr::group_by(generation) %>%
    dplyr::summarise(fst = mean(fst),
                     .groups = "drop") %>%
    dplyr::mutate(popsize = pop_mean) %>%
    dplyr::filter(generation > 750) ## only take data after equilibrium reached
  
  ggplot(fst_pop, aes(popsize, fst)) +
    geom_path(aes(colour = generation), 
              arrow = arrow(length = unit(0.15, "cm"), type = "closed"), 
              alpha = 0.5) +
    annotate("point", x = fst_pop$popsize[1], fst_pop$fst[1], colour = "red", size = 2) +
    scale_x_log10() +
    scale_colour_viridis_c() +
    theme_minimal()
}

plot_cycle(fst_dat, pop_mean)

The plot above shows how Fst and population size are changing through time together for 250 generations (the red dot is the starting point at generation 750). It appears that as population size begins increasing the Fst begins to drop slightly. This drop in Fst continues even as population size begins to decrease again (a lag effect?), then Fst suddenly increases back to nearly 1.0 as population size reaches a low. I suspect this pattern is due to ephemeral effects of increased efficiency of selection when population size is high. As new mutations begin sweeping to fixation there is an intermediate period where they are at intermediate frequency – this has the tendency to lead to reductions in within-population heterozygosity (relative to total heterozygosity), and leads to the subpopulation becoming slightly more similar due to chance collisions between genes being selected in both populations.

Due to overplotting however, some of the details of the plot may be difficult to see. Instead let’s use gganimate to create an animated version that might show the dynamics a bit better.

animate_cycle <- function(fst_dat, pop_mean) {
  fst_pop <- fst_dat %>%
    dplyr::group_by(generation) %>%
    dplyr::summarise(fst = mean(fst), .groups = "drop") %>%
    dplyr::mutate(popsize = pop_mean) %>%
    dplyr::filter(generation > 250) 
  
  anim <- ggplot(fst_pop, aes(popsize, fst)) +
    geom_point() +
    scale_x_log10() +
    transition_reveal(generation) +
    shadow_wake(0.02, falloff = "sine-in") +
    ggtitle("Generation: {frame_along}") +
    theme_minimal()
  
  animate(anim, nframes = n_distinct(fst_pop$generation) * 5, fps = 30, 
          start_pause = 10, end_pause = 10, renderer = gifski_renderer())
}

animate_cycle(fst_dat, pop_mean)

The above animation shows 750 generations of the dynamics, with the black dot showing the position of the population in terms of the mean population size, and the mean pairwise Fst, at different time points. We can clearly see the cycles occurring and its direction.

In any event, this is obviously a pretty unrealistic scenario, and it bears little resemblance to what is happening in our actual data. So, let us try a few other sets of parameters, and see if we can get something closer to what we observe in our data. Now we can finally see how we specify non-default values to our templated variables using slim_script_render. Let’s start by changing the migration_rate parameter, which controls how much migration we get between subpopulations when they go over their threshold population size. Let’s set it to maximum migration (0.5) – this model is similar to our hypothesis for the pattern we see. We will also set selection to practically zero, so we can isolate drift as a mechanism. We set a parameter value in our script like this:

## set migration rate to 0.5 (maximum migration, e.g. 50% of population switches place)
## population will become panmictic over a certain popsize threshold
fst_script_2 <- slim_script_render(pop_sim_fst, template = list(migration_rate = 0.5,
                                                                 selection_strength = 1e-12))
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
## now run the sim and show plots of results
fst_res_2 <- slim_run(fst_script_2, throw_error = TRUE)
## 
## 
## Simulation finished with exit status: 0
## 
## Success!
fst_dat_2 <- extract_fst(fst_res_2)

ggplot(fst_dat_2, aes(generation, fst)) +
  geom_path(aes(colour = `subpop\npair`)) +
  scale_y_sqrt() +
  ggforce::facet_zoom(x = generation > 750 & generation < 1000,
                      y = fst < 0.5,
                      horizontal = FALSE,
                      zoom.size = 1)

plot_cycle(fst_dat_2, pop_mean)

animate_cycle(fst_dat_2, pop_mean)

This shows a very distinct pattern relative to our last run. Now we see that Fst tends to be very low, particularly when population sizes are high (e.g. when migration is “turned on”), as expected. Fst increases in the intervening periods of low population size, which can only be explained by drift. Our Fst values are closer to the magnitude of values we see in our data, which max out at around 0.05, though they still tend to be much higher in this simulation overall.

Let’s try one more scenario before we attempt to more formally compare the simulations to our data. Let’s set our abund_threshold controlling at what population size the subpopulation begins exchanging individuals to zero. This means that migration will be on all the time. We will also increase selection.

fst_script_3 <- slim_script_render(pop_sim_fst, template = list(migration_rate = 0.5,
                                                                 selection_strength = 0.2,
                                                                 abund_threshold = 0))
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
fst_res_3 <- slim_run(fst_script_3, throw_error = TRUE)
## 
## 
## Simulation finished with exit status: 0
## 
## Success!
fst_dat_3 <- extract_fst(fst_res_3)

ggplot(fst_dat_3, aes(generation, fst)) +
  geom_path(aes(colour = `subpop\npair`)) +
  scale_y_sqrt() +
  ggforce::facet_zoom(x = generation > 750 & generation < 1000,
                      y = fst < 0.2,
                      horizontal = FALSE,
                      zoom.size = 1)

plot_cycle(fst_dat_3, pop_mean)

animate_cycle(fst_dat_3, pop_mean)

We can again see that we get rapid fluctuations in Fst relating to changes in population size. Here they are a bit more rapid but don’t reach the same extremes of Fst. The fluctuations presumably still occur because at very low population size, drift still allows the subpopulations to drift apart due to chance alone, even though they are panmictic (in other words, if you choose any two random subsets of individuals you would expect a higher pairwise Fst during times of low population size). Before we move on, what happens if we reduce the migration rate with an abundance threshold:

fst_script_4 <- slim_script_render(pop_sim_fst, template = list(migration_rate = 0.2,
                                                                 selection_strength = 1e-12,
                                                                 abund_threshold = 5))
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
fst_res_4 <- slim_run(fst_script_4, throw_error = TRUE)
## 
## 
## Simulation finished with exit status: 0
## 
## Success!
fst_dat_4 <- extract_fst(fst_res_4)

ggplot(fst_dat_4, aes(generation, fst)) +
  geom_path(aes(colour = `subpop\npair`)) +
  scale_y_sqrt() +
  ggforce::facet_zoom(x = generation > 750 & generation < 1000,
                      y = fst < 0.2,
                      horizontal = FALSE,
                      zoom.size = 1)

plot_cycle(fst_dat_4, pop_mean)

animate_cycle(fst_dat_4, pop_mean)

Again we see those fluctuations. All in all, this suggests that it is likely to be difficult to distinguish what processes are driving the pattern of Fst we see in our study. There clearly are some differences in the details of the dynamics, particularly when you compare our cycle plots between the different scenarios. However, we only have three years of data, just spanning a rainfall (or population explosion) event, which means considerably less information to go on. It looks like it is going to be difficult to disentangle the effects of migration from just the effects of population size, which is perhaps not surprising, given that these two factors are linked in our model (e.g. our hypothesis is that migration increases with high rainfall, which also is linked with population increases). Unfortunately the two processes have a similar effect on Fst, migration creates gene flow which reduces population differentiation, but increasing population increases effective population size, and thus reducing the strength of drift relative to selection, me

So the next thing we should do is get our simulation to output data we can compare directly to our data. In this case we will just output full population information at several time points, and calculate Fst post-hoc using our custom R function to make sure that our methods of calculating Fst are the same. We will also sample the subpopulations in our simulation to try and match sample size with our real data as well.

samp_sizes <- table(gen$other$ind.metrics$three_pop, gen$other$ind.metrics$year)

samp_sizes <- do.call(cbind, replicate(6, samp_sizes, simplify = FALSE))

## save for later
#write_rds(samp_sizes, "data/samp_sizes.rds")

slim_script(
  
  slim_block(initialize(), {
    
    #setSeed(12345); don't want to set the seed because we want diff each time
    #initializeSLiMOptions(keepPedigrees=T);
    initializeMutationRate(slimr_template("mut_rate", 1e-6));
    initializeMutationType("m1", 0.5, "n", 0, slimr_template("selection_strength", 0.1));
    initializeGenomicElementType("g1", m1, 1.0);
    initializeGenomicElement(g1, 0, slimr_template("genome_size", !!default_genome_size) - 1);
    initializeRecombinationRate(slimr_template("recomb_rate", 1e-8));
    initializeSex("A");
    defineConstant("abund", slimr_inline(pop_abunds, delay = FALSE));
    defineConstant("sample_these", slimr_inline(sample_these, delay = FALSE));
    defineConstant("samp_sizes", slimr_inline(samp_sizes, delay = FALSE));
    
  }),
  slim_block(1, {
    
    init_pop = slimr_inline(init_popsize, delay = TRUE)
    
    ## set populations to initial size
    sim.addSubpop("p1", asInteger(init_pop[0]));
    sim.addSubpop("p2", asInteger(init_pop[1]));
    sim.addSubpop("p3", asInteger(init_pop[2]));
    
  }),
  
  slim_block(1, late(), {
    ## get starting population from a file which we will fill-in later
    sim.readFromPopulationFile(slimr_inline(starting_pop, delay = TRUE));
    ## migration on or off flags for pops 1-3 (using tag)
    p1.tag = 0;
    p2.tag = 0;
    p3.tag = 0;
  }),
  
  slim_block(1, 1000, late(), {
    
    ## update generation number
    ## note in SLiM versions < 4.0, this would be sim.generation instead
    gen = sim.cycle %% 50
    if(gen == 0) {
      gen = 50
    }
    
    ## set population size to observed levels
    p1.setSubpopulationSize(asInteger(ceil(abund[0, gen - 1] * slimr_template("popsize_scaling", 100))));
    p2.setSubpopulationSize(asInteger(ceil(abund[1, gen - 1] * ..popsize_scaling..)));
    p3.setSubpopulationSize(asInteger(ceil(abund[2, gen - 1] * ..popsize_scaling..)));
    
    ## increase migration when above abundance threshold
    if(p1.tag == 0 & abund[0, gen - 1] > slimr_template("abund_threshold", 5)) {
      p2.setMigrationRates(p1, slimr_template("migration_rate", 0))
      p3.setMigrationRates(p1, ..migration_rate..)
      p1.tag = 1;
    } 
    if(p1.tag == 1 & abund[0, gen - 1] <= ..abund_threshold..) {
      p2.setMigrationRates(p1, 0)
      p3.setMigrationRates(p1, 0)
      p1.tag = 0;
    }
    
    if(p2.tag == 0 & abund[1, gen - 1] > ..abund_threshold..) {
      p1.setMigrationRates(p2, ..migration_rate..)
      p3.setMigrationRates(p2, ..migration_rate..)
      p2.tag = 1;
    } 
    if(p2.tag == 1 & abund[1, gen - 1] <= ..abund_threshold..) {
      p1.setMigrationRates(p2, 0)
      p3.setMigrationRates(p2, 0)
      p2.tag = 0;
    }    
    
    if(p3.tag == 0 & abund[2, gen - 1] > ..abund_threshold..) {
      p1.setMigrationRates(p3, ..migration_rate..)
      p2.setMigrationRates(p3, ..migration_rate..)
      p3.tag = 1;
    } 
    if(p3.tag == 1 & abund[2, gen - 1] <= ..abund_threshold..) {
      p1.setMigrationRates(p3, 0)
      p2.setMigrationRates(p3, 0)
      p3.tag = 0;
    }
    
    ## only run if the generation (cycle) is in our sample_these list
    ## note in SLiM versions < 4.0, this would be sim.generation instead of sim.cycle
    if(any(match(sample_these, sim.cycle) >= 0)) {
      ## find the sample size that matches the matching "year" for our obs data
      ssizes = drop(samp_sizes[ , which(sample_these == sim.cycle)]) 
      ## sample individuals
      ind_sample = c(sample(p1.individuals, ssizes[0]),
                     sample(p2.individuals, ssizes[1]),
                     sample(p3.individuals, ssizes[2]))
      ## output individuals genomes
      slimr_output(ind_sample.genomes.output(), "pop_sample", do_every = 1);
      slimr_output(ind_sample.genomes.individual.subpopulation, "subpops", do_every = 1)
    }
    
  }),
  
  slim_block(1000, late(), {
    sim.simulationFinished()
  })
  
) -> pop_sim_samp

Let’s give it a try! Here we will demonstrate another feature of slimr. By specifying a reps argument to slim_script_render we can generate replicate simulations. This returns a slimr_script_coll object, which stands for slimr script collection. This can be run using slim_run just like a regular slimr_script, but each simulation in the collection will be run, either sequentially, or in parallel, depending on what argument we use. We will run in parallel on 6 cores, and run 6 replicates of our entire simulation, with the same parameters. So then we will have 6 replicate samples within each simulation and 6 replicate simulation for a total of 36 replicates for each of our 3 sampling time points.

To run in parallel we must first set a plan for the package future, which slimr uses under the hood for parallelism:

plan(multisession(workers = 6))

Now we can run our script!

samp_script <- slim_script_render(pop_sim_samp, template = list(migration_rate = 0.5,
                                                                 selection_strength = 1e-12,
                                                                 abund_threshold = 0),
                                   reps = 6)
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
samp_res <- slim_run(samp_script, throw_error = TRUE,
                     parallel = TRUE)

Just to confirm we sampled the generations we wanted:

unique(samp_res[[1]]$output_data$generation)
##  [1] 740 745 749 790 795 799 840 845 849 890 895 899 940 945 949 990 995 999
sample_these
##  [1] 740 745 749 790 795 799 840 845 849 890 895 899 940 945 949 990 995 999

The last step is to take the output of the simulation, convert it into a set of genlight objects, and then calculate Fst on those using the same function used to calculate Fst for our real data. Then we can plot a distribution of expected Fst values at our three time points based on this simulation, and compare that with our real data. We can convert the genomic information to genlight using slim_extract_genlight(). By specifying the name argument, slim_extract_genlight() will only use rows with that name. Additionally we can create a separate genlight for each generation sample by using the by argument, like this:

slim_genlights <- slim_extract_genlight(samp_res[[1]], name = "pop_sample",
                                       by = "generation")

slim_genlights
## # A tibble: 18 × 2
##    generation genlight        
##         <int> <list>          
##  1        740 <genlight[,509]>
##  2        745 <genlight[,750]>
##  3        749 <genlight[,624]>
##  4        790 <genlight[,519]>
##  5        795 <genlight[,754]>
##  6        799 <genlight[,654]>
##  7        840 <genlight[,308]>
##  8        845 <genlight[,525]>
##  9        849 <genlight[,427]>
## 10        890 <genlight[,347]>
## 11        895 <genlight[,550]>
## 12        899 <genlight[,501]>
## 13        940 <genlight[,408]>
## 14        945 <genlight[,652]>
## 15        949 <genlight[,538]>
## 16        990 <genlight[,548]>
## 17        995 <genlight[,810]>
## 18        999 <genlight[,664]>

We now have a tibble containing genlight objects for each generation sample. Let’s plot one to make sure it worked.

plot(slim_genlights$genlight[[1]])

Now because we output a sample of individuals’ genomes in our simulation, and SLiM doesn’t include information on the subpopulation of origin in its genome output, we also output the associated subpopulation for each genome and included it the "subpops" data rows. We can now extract this and reassociate with the genome data. This is possible because SLiM always outputs this data in the same order. Note that we sampled 50 individuals which means we have 100 genomes. These were reassembled into individuals by slim_extract_genlight(), and we can likewise aggregate our subpopulation information by noting that SLiM always outputs the two genomes from a single individual one after the other. We can use the slimr function slim_results_to_data() function which automatically attempts to convert the data stored in our results object to usable data in the form of a tibble. This also works with a

slim_subpops <- samp_res[[1]]$output_data %>%
  filter(name == "subpops") %>%
  slim_results_to_data() 

slim_subpops 
## # A tibble: 18 × 5
##    type                                        expression generation name  data 
##    <chr>                                       <chr>           <int> <chr> <lis>
##  1 "object<Subpopulation> [0:73] Subpopulatio… ind_sampl…        740 subp… <chr>
##  2 "object<Subpopulation> [0:155] Subpopulati… ind_sampl…        745 subp… <chr>
##  3 "object<Subpopulation> [0:65] Subpopulatio… ind_sampl…        749 subp… <chr>
##  4 "object<Subpopulation> [0:73] Subpopulatio… ind_sampl…        790 subp… <chr>
##  5 "object<Subpopulation> [0:155] Subpopulati… ind_sampl…        795 subp… <chr>
##  6 "object<Subpopulation> [0:65] Subpopulatio… ind_sampl…        799 subp… <chr>
##  7 "object<Subpopulation> [0:73] Subpopulatio… ind_sampl…        840 subp… <chr>
##  8 "object<Subpopulation> [0:155] Subpopulati… ind_sampl…        845 subp… <chr>
##  9 "object<Subpopulation> [0:65] Subpopulatio… ind_sampl…        849 subp… <chr>
## 10 "object<Subpopulation> [0:73] Subpopulatio… ind_sampl…        890 subp… <chr>
## 11 "object<Subpopulation> [0:155] Subpopulati… ind_sampl…        895 subp… <chr>
## 12 "object<Subpopulation> [0:65] Subpopulatio… ind_sampl…        899 subp… <chr>
## 13 "object<Subpopulation> [0:73] Subpopulatio… ind_sampl…        940 subp… <chr>
## 14 "object<Subpopulation> [0:155] Subpopulati… ind_sampl…        945 subp… <chr>
## 15 "object<Subpopulation> [0:65] Subpopulatio… ind_sampl…        949 subp… <chr>
## 16 "object<Subpopulation> [0:73] Subpopulatio… ind_sampl…        990 subp… <chr>
## 17 "object<Subpopulation> [0:155] Subpopulati… ind_sampl…        995 subp… <chr>
## 18 "object<Subpopulation> [0:65] Subpopulatio… ind_sampl…        999 subp… <chr>
slim_subpops <- slim_subpops %>%
  unnest_longer(col = data, values_to = "subpop", indices_to = "genome_num") %>%
  mutate(individual_num = c(rep(1:sum(samp_sizes[ , 1]), each = 2),
                            rep(1:sum(samp_sizes[ , 2]), each = 2),
                            rep(1:sum(samp_sizes[ , 3]), each = 2)) %>%
           rep(6))

slim_subpops
## # A tibble: 1,776 × 7
##    type             expression generation name  subpop genome_num individual_num
##    <chr>            <chr>           <int> <chr> <chr>       <int>          <int>
##  1 "object<Subpopu… ind_sampl…        740 subp… Subpo…          1              1
##  2 "object<Subpopu… ind_sampl…        740 subp… Subpo…          2              1
##  3 "object<Subpopu… ind_sampl…        740 subp… Subpo…          3              2
##  4 "object<Subpopu… ind_sampl…        740 subp… Subpo…          4              2
##  5 "object<Subpopu… ind_sampl…        740 subp… Subpo…          5              3
##  6 "object<Subpopu… ind_sampl…        740 subp… Subpo…          6              3
##  7 "object<Subpopu… ind_sampl…        740 subp… Subpo…          7              4
##  8 "object<Subpopu… ind_sampl…        740 subp… Subpo…          8              4
##  9 "object<Subpopu… ind_sampl…        740 subp… Subpo…          9              5
## 10 "object<Subpopu… ind_sampl…        740 subp… Subpo…         10              5
## # ℹ 1,766 more rows
slim_subpops <- slim_subpops %>%
  group_by(generation, individual_num) %>%
  summarise(subpop = subpop[1],
            .groups = "drop")

slim_subpops
## # A tibble: 888 × 3
##    generation individual_num subpop           
##         <int>          <int> <chr>            
##  1        740              1 Subpopulation<p1>
##  2        740              2 Subpopulation<p1>
##  3        740              3 Subpopulation<p1>
##  4        740              4 Subpopulation<p1>
##  5        740              5 Subpopulation<p1>
##  6        740              6 Subpopulation<p1>
##  7        740              7 Subpopulation<p1>
##  8        740              8 Subpopulation<p1>
##  9        740              9 Subpopulation<p1>
## 10        740             10 Subpopulation<p1>
## # ℹ 878 more rows

Okay, now we can add this to the genlights we generated earlier.

## reorder by individual label
slim_genlights$genlight <- map(slim_genlights$genlight,
                               ~.x[order(as.numeric(.x$ind.names)), ])
## add subpop data
slim_genlights$genlight <- map2(slim_genlights$genlight, 
                                slim_subpops %>%
                                  group_split(generation),
                                ~{pop(.x) <- .y$subpop; .x})

Let’s look at that plot again but reordered by population.

test_gl <- slim_genlights$genlight[[1]]
test_gl <- test_gl[order(pop(test_gl)), ]
plot(test_gl)

Now we are ready to calculate our pairwise Fsts for each sample.

fst_by_generation_sim <- map(slim_genlights$genlight,
                   ~pairFST(.x) %>%
                     as.dist() %>%
                     as.matrix())
names(fst_by_generation_sim) <- slim_genlights$generation
fst_by_generation_sim
## $`740`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>       0.000000000       0.006868622        0.01071799
## Subpopulation<p2>       0.006868622       0.000000000        0.01590407
## Subpopulation<p3>       0.010717986       0.015904072        0.00000000
## 
## $`745`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>       0.000000000       0.007452122       0.007390905
## Subpopulation<p2>       0.007452122       0.000000000       0.009817015
## Subpopulation<p3>       0.007390905       0.009817015       0.000000000
## 
## $`749`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.01224182        0.01722751
## Subpopulation<p2>        0.01224182        0.00000000        0.01584085
## Subpopulation<p3>        0.01722751        0.01584085        0.00000000
## 
## $`790`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>       0.000000000       0.008190359        0.02748787
## Subpopulation<p2>       0.008190359       0.000000000        0.02376266
## Subpopulation<p3>       0.027487866       0.023762659        0.00000000
## 
## $`795`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000       0.019101570       0.015305827
## Subpopulation<p2>        0.01910157       0.000000000       0.007011274
## Subpopulation<p3>        0.01530583       0.007011274       0.000000000
## 
## $`799`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.02937367        0.02953601
## Subpopulation<p2>        0.02937367        0.00000000        0.01304019
## Subpopulation<p3>        0.02953601        0.01304019        0.00000000
## 
## $`840`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.01790289        0.03155570
## Subpopulation<p2>        0.01790289        0.00000000        0.04443851
## Subpopulation<p3>        0.03155570        0.04443851        0.00000000
## 
## $`845`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>       0.000000000       0.008468425       0.009469408
## Subpopulation<p2>       0.008468425       0.000000000       0.007178540
## Subpopulation<p3>       0.009469408       0.007178540       0.000000000
## 
## $`849`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.05451409        0.03534917
## Subpopulation<p2>        0.05451409        0.00000000        0.01496291
## Subpopulation<p3>        0.03534917        0.01496291        0.00000000
## 
## $`890`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.01805284        0.06063659
## Subpopulation<p2>        0.01805284        0.00000000        0.09274040
## Subpopulation<p3>        0.06063659        0.09274040        0.00000000
## 
## $`895`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>       0.000000000        0.03619680       0.007898948
## Subpopulation<p2>       0.036196796        0.00000000       0.025193171
## Subpopulation<p3>       0.007898948        0.02519317       0.000000000
## 
## $`899`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.03030347        0.02855643
## Subpopulation<p2>        0.03030347        0.00000000        0.02647997
## Subpopulation<p3>        0.02855643        0.02647997        0.00000000
## 
## $`940`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.01182880        0.02001984
## Subpopulation<p2>        0.01182880        0.00000000        0.02763523
## Subpopulation<p3>        0.02001984        0.02763523        0.00000000
## 
## $`945`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>       0.000000000       0.012288572       0.007830131
## Subpopulation<p2>       0.012288572       0.000000000       0.008468013
## Subpopulation<p3>       0.007830131       0.008468013       0.000000000
## 
## $`949`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.02418577        0.03688533
## Subpopulation<p2>        0.02418577        0.00000000        0.01777150
## Subpopulation<p3>        0.03688533        0.01777150        0.00000000
## 
## $`990`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000        0.02521869        0.03493360
## Subpopulation<p2>        0.02521869        0.00000000        0.03465002
## Subpopulation<p3>        0.03493360        0.03465002        0.00000000
## 
## $`995`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>        0.00000000       0.011318902       0.010413951
## Subpopulation<p2>        0.01131890       0.000000000       0.005724975
## Subpopulation<p3>        0.01041395       0.005724975       0.000000000
## 
## $`999`
##                   Subpopulation<p1> Subpopulation<p2> Subpopulation<p3>
## Subpopulation<p1>         0.0000000        0.02618320        0.02459370
## Subpopulation<p2>         0.0261832        0.00000000        0.03405698
## Subpopulation<p3>         0.0245937        0.03405698        0.00000000

So in this simulation we have very low pairwise fst, including a fair number of negative numbers. This is expected given the high level of migration we used in the simulation (essentially we simulated just one panmictic population, with very weak selection). Now we can write a little function to summarise these results, and plot them along with our observed values. We will also write some new code to get the data from all 6 of our simulation replicates and combine them. Then we can run a few more simulations with different parameters and see how they stack up!

sim_result <- samp_res
observed_fst <- fst_df
plot_fst_comparison <- function(sim_result, observed_fst, tech_reps = 6L) {
  slim_genlights <- slim_extract_genlight(sim_result, name = "pop_sample",
                                       by = "generation")
  
  names(sim_result) <- paste0("rep_", seq_along(sim_result))
  
  slim_subpops <- imap_dfr(sim_result, ~ .x$output_data %>%
                            filter(name == "subpops") %>%
                            filter(!duplicated(generation)) %>% ## deals with an intermittent bug
                            slim_results_to_data() %>%
                            unnest_longer(col = data, values_to = "subpop",
                                          indices_to = "genome_num") %>%
                            mutate(individual_num = c(rep(1:sum(samp_sizes[ ,
                                                                            1]), each = 2),
                                                      rep(1:sum(samp_sizes[ , 2]), each = 2),
                                                      rep(1:sum(samp_sizes[ , 3]), each = 2)) %>%
                                     rep(tech_reps),
                                   rep = .y) %>%
                            group_by(generation, individual_num, rep) %>%
                            summarise(subpop = subpop[1],
                                      .groups = "drop"))
  
  ## reorder by individual label
  slim_genlights$genlight <- map(slim_genlights$genlight,
                                 ~.x[order(as.numeric(.x$ind.names)), ])
  ## add subpop data
  slim_genlights$genlight <- map2(slim_genlights$genlight, 
                                  slim_subpops %>%
                                    group_split(rep, generation),
                                  ~{pop(.x) <- .y$subpop; .x})
  
  
  fst_by_year_sim <- map(slim_genlights$genlight,
                     ~pairFST(.x) %>%
                       as.dist() %>%
                       as.matrix())
  years <- rep(c("2006", "2007", "2008"), tech_reps * 6)
  
  
  fst_sim_df <- imap_dfr(fst_by_year_sim,
                     ~combn(c("Subpopulation<p1>", 
                              "Subpopulation<p2>", 
                              "Subpopulation<p3>"), 2) %>%
                       t() %>%
                       as.data.frame() %>%
                       rename(pop1 = V1, pop2 = V2) %>%
                       mutate(fst = .x[cbind(pop1, pop2)],
                              year = years[.y],
                              rep = slim_genlights$rep[.y],
                              generation = slim_genlights$generation[.y],
                              pop_combo = paste(pop1, pop2, sep = " to "))) %>%
    group_by(rep, generation, year) %>%
    summarise(fst = mean(fst), .groups = "drop") %>%
    mutate(iter = rep(1:(tech_reps*6), each = 3))
  
  all_summ <- fst_sim_df %>%
    group_by(year) %>%
    summarise(fst_mean = mean(fst), fst_sd = sd(fst),
              fst = fst_mean,
              .groups = "drop")
  
  fst_obs_df <- observed_fst %>%
    group_by(year) %>%
    summarise(fst_mean = mean(fst),
              fst = fst_mean,
              iter = 99999,
              .groups = "drop")
  
  fst_plot_df1 <- bind_rows(fst_sim_df %>% mutate(type = "simulated"),
                           fst_obs_df %>% mutate(type = "observed")) 
  
  fst_plot_df2 <- bind_rows(all_summ %>% mutate(type = "simulated"),
                           fst_obs_df %>% mutate(type = "observed")) 
  
  p1 <- ggplot(fst_plot_df1, aes(year, fst)) +
    geom_errorbar(aes(ymin = fst_mean - fst_sd,
                      ymax = fst_mean + fst_sd),
                  width = 0.1,
                  data = all_summ) +
    geom_point(aes(colour = type, alpha = type, size = type)) +
    geom_path(aes(colour = type, alpha = type, group = iter)) +
    geom_point(aes(year, fst),  size = 2,
               data = all_summ, inherit.aes = FALSE) +
    geom_path(aes(year, fst), group = 1, data = all_summ, 
              inherit.aes = FALSE) +
    scale_alpha_manual(values = c(1, 0.5)) +
    scale_size_manual(values = c(2, 1)) +
    theme_minimal()
    
  p1
  
}

plot_fst_comparison(samp_res, fst_df)

This function plots the means of the pairwise Fst calculated from the simulation along with error bars representing 2 standard deviations around the mean, and the blue lines are each of the 36 replicates (6 temporal times 6 simulation runs). The red dots are the observed data. In this case, the match is not bad, the simulation generates a drop is Fst in 2007, with a rebound in 2008, but overall the Fst values are too low and the rebound is too strong. Let’s try running our simulation again with some of the parameter combinations we explored earlier and see how this plot looks for them!

## set migration rate to 0.5 (maximum migration, e.g. 50% of population switches place)
## population will become panmictic over a certain popsize threshold
fst_samp_2 <- slim_script_render(pop_sim_samp, template = list(migration_rate = 0.5,
                                                                selection_strength = 1e-12),
                                  reps = 6)
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
## now run the sim and show plots of results
samp_res_2 <- slim_run(fst_samp_2, throw_error = TRUE,
                       parallel = TRUE)
plot_fst_comparison(samp_res_2, fst_df)

Okay, well that simulation produces Fsts within a similar average range as the observed, but interestingly Fst is too high in 2006 and then too low in 2007 and 2008 during and after the rainfall event. This is something that we couldn’t tell from the plots of Fst over time, which showed fluctuations but did not show the detail of exactly when the fluctuations occurred. Let’s try another set of parameters!

## set migration rate to 0.5 (maximum migration, e.g. 50% of population switches place)
## population is panmictic all the time with abund_threshold = 0,
## now there is stronger selection too
fst_samp_3 <- slim_script_render(pop_sim_samp, template = list(migration_rate = 0.5,
                                                                selection_strength = 0.2,
                                                                abund_threshold = 0),
                                  reps = 6)
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
## now run the sim and show plots of results
samp_res_3 <- slim_run(fst_samp_3, throw_error = TRUE,
                       parallel = TRUE)
plot_fst_comparison(samp_res_3, fst_df)

Now the pattern across years is again similar to the real data, though again the rebound is a little too strong and again overall Fst values are too low. To me, the second simulation that was based most strongly on our hypothesis was quite promising. Let’s tweak the parameters of that one slightly to see if we can get a bit better fit.

fst_samp_4 <- slim_script_render(pop_sim_samp, template = list(migration_rate = 0.3,
                                                                selection_strength = 1e-12,
                                                                abund_threshold = 2),
                                  reps = 6)
## Warning: Warning: A templated variable was not specified in the template and has been replaced by its default value.
## now run the sim and show plots of results
samp_res_4 <- slim_run(fst_samp_4, throw_error = TRUE,
                       parallel = TRUE)
plot_fst_comparison(samp_res_4, fst_df)

So that is not much better. So far, we haven’t found a perfect simulation, but we have discovered that quite a range of different evolutionary scenarios can create fairly similar patterns. The next step may be to do some formal fitting of the simulation data to the observed using a method such as Approximate Bayesian Computation (ABC). This would be straightforward using slimr, since R already has good packages for performing ABC with custom simulation code. It may be the case that for distinguishing the evolutionary forces we are interested in here that we may need more data, or data of a different kind. We could also use simulation to further ask, is there anything we could measure or calculate, besides Fst, from the genomes of individuals, that could better distinguish multiple evolutionary forces? I will leave that project up to future work, but hopefully I have demonstrated the potential of having a tighter integration between data analysis platforms and simulation software.

Last but not least, this is the session information for this run of the above code. We can only guarantee reproducibility of this document if the session where it is being run matches the following:

## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.3.2 (2023-10-31 ucrt)
##  os       Windows 11 x64 (build 22000)
##  system   x86_64, mingw32
##  ui       RTerm
##  language en
##  collate  English_Australia.utf8
##  ctype    English_Australia.utf8
##  tz       America/New_York
##  date     2023-12-23
##  pandoc   3.1.1 @ C:/Program Files/RStudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package           * version    date (UTC) lib source
##  ade4              * 1.7-22     2023-02-06 [2] CRAN (R 4.3.2)
##  adegenet          * 2.1.10     2023-01-26 [2] CRAN (R 4.3.2)
##  ape                 5.7-1      2023-03-13 [2] CRAN (R 4.3.2)
##  base64enc           0.1-3      2015-07-28 [2] CRAN (R 4.3.1)
##  bit                 4.0.5      2022-11-15 [2] CRAN (R 4.3.2)
##  bit64               4.0.5      2020-08-30 [2] CRAN (R 4.3.2)
##  brew                1.0-8      2022-09-29 [2] CRAN (R 4.3.2)
##  bslib               0.5.1      2023-08-11 [2] CRAN (R 4.3.2)
##  cachem              1.0.8      2023-05-01 [2] CRAN (R 4.3.2)
##  calibrate           1.7.7      2020-06-19 [2] CRAN (R 4.3.2)
##  class               7.3-22     2023-05-03 [2] CRAN (R 4.3.2)
##  classInt            0.4-10     2023-09-05 [2] CRAN (R 4.3.2)
##  cli                 3.6.1      2023-03-23 [2] CRAN (R 4.3.2)
##  cluster             2.1.4      2022-08-22 [2] CRAN (R 4.3.2)
##  codetools           0.2-19     2023-02-01 [2] CRAN (R 4.3.2)
##  colorspace          2.1-0      2023-01-23 [2] CRAN (R 4.3.2)
##  combinat            0.0-8      2012-10-29 [2] CRAN (R 4.3.1)
##  conflicted        * 1.2.0      2023-02-01 [2] CRAN (R 4.3.2)
##  crayon              1.5.2      2022-09-29 [2] CRAN (R 4.3.2)
##  crosstalk           1.2.0      2021-11-04 [2] CRAN (R 4.3.2)
##  dartR             * 2.9.7      2023-06-07 [2] CRAN (R 4.3.2)
##  dartR.data        * 1.0.2      2022-11-16 [2] CRAN (R 4.3.2)
##  data.table          1.14.8     2023-02-17 [2] CRAN (R 4.3.2)
##  DBI                 1.1.3      2022-06-18 [2] CRAN (R 4.3.2)
##  desc                1.4.2      2022-09-08 [2] CRAN (R 4.3.2)
##  digest              0.6.33     2023-07-07 [2] CRAN (R 4.3.2)
##  directlabels      * 2023.8.25  2023-09-01 [2] CRAN (R 4.3.2)
##  dismo               1.3-14     2023-05-21 [2] CRAN (R 4.3.2)
##  doParallel          1.0.17     2022-02-07 [2] CRAN (R 4.3.2)
##  dotCall64           1.1-0      2023-10-17 [2] CRAN (R 4.3.2)
##  dplyr             * 1.1.3      2023-09-03 [2] CRAN (R 4.3.2)
##  e1071               1.7-13     2023-02-01 [2] CRAN (R 4.3.2)
##  ellipsis            0.3.2      2021-04-29 [2] CRAN (R 4.3.2)
##  evaluate            0.23       2023-11-01 [2] CRAN (R 4.3.2)
##  fansi               1.0.5      2023-10-08 [2] CRAN (R 4.3.2)
##  farver              2.1.1      2022-07-06 [2] CRAN (R 4.3.2)
##  fastmap             1.1.1      2023-02-24 [2] CRAN (R 4.3.2)
##  fields              15.2       2023-08-17 [2] CRAN (R 4.3.2)
##  foreach             1.5.2      2022-02-02 [2] CRAN (R 4.3.2)
##  fs                  1.6.3      2023-07-20 [2] CRAN (R 4.3.2)
##  furrr             * 0.3.1      2022-08-15 [2] CRAN (R 4.3.2)
##  future            * 1.33.0     2023-07-01 [2] CRAN (R 4.3.2)
##  gap                 1.5-3      2023-08-26 [2] CRAN (R 4.3.2)
##  gap.datasets        0.0.6      2023-08-25 [2] CRAN (R 4.3.1)
##  gdata               3.0.0      2023-10-16 [2] CRAN (R 4.3.2)
##  gdistance           1.6.4      2023-06-19 [2] CRAN (R 4.3.2)
##  gdsfmt            * 1.38.0     2023-10-24 [2] Bioconductor
##  generics            0.1.3      2022-07-05 [2] CRAN (R 4.3.2)
##  genetics            1.3.8.1.3  2021-03-01 [2] CRAN (R 4.3.2)
##  GGally              2.1.2      2021-06-21 [2] CRAN (R 4.3.2)
##  gganimate         * 1.0.8      2022-09-08 [2] CRAN (R 4.3.2)
##  ggforce           * 0.4.1      2022-10-04 [2] CRAN (R 4.3.2)
##  ggplot2           * 3.4.4      2023-10-12 [2] CRAN (R 4.3.2)
##  gifski              1.12.0-2   2023-08-12 [2] CRAN (R 4.3.2)
##  globals             0.16.2     2022-11-21 [2] CRAN (R 4.3.1)
##  glue                1.6.2      2022-02-24 [2] CRAN (R 4.3.2)
##  gridExtra           2.3        2017-09-09 [2] CRAN (R 4.3.2)
##  gtable              0.3.4      2023-08-21 [2] CRAN (R 4.3.2)
##  gtools              3.9.4      2022-11-27 [2] CRAN (R 4.3.2)
##  here              * 1.0.1      2020-12-13 [2] CRAN (R 4.3.2)
##  highr               0.10       2022-12-22 [2] CRAN (R 4.3.2)
##  hms                 1.1.3      2023-03-21 [2] CRAN (R 4.3.2)
##  htmltools           0.5.7      2023-11-03 [2] CRAN (R 4.3.2)
##  htmlwidgets         1.6.2      2023-03-17 [2] CRAN (R 4.3.2)
##  httpuv              1.6.12     2023-10-23 [2] CRAN (R 4.3.2)
##  igraph              1.5.1      2023-08-10 [2] CRAN (R 4.3.2)
##  iterators           1.0.14     2022-02-05 [2] CRAN (R 4.3.2)
##  jquerylib           0.1.4      2021-04-26 [2] CRAN (R 4.3.2)
##  jsonlite            1.8.7      2023-06-29 [2] CRAN (R 4.3.2)
##  KernSmooth          2.23-22    2023-07-10 [2] CRAN (R 4.3.2)
##  knitr               1.45       2023-10-30 [2] CRAN (R 4.3.2)
##  labeling            0.4.3      2023-08-29 [2] CRAN (R 4.3.1)
##  later               1.3.1      2023-05-02 [2] CRAN (R 4.3.2)
##  lattice             0.21-9     2023-10-01 [2] CRAN (R 4.3.2)
##  leafem              0.2.3      2023-09-17 [2] CRAN (R 4.3.2)
##  leaflet             2.2.1      2023-11-13 [2] CRAN (R 4.3.2)
##  leaflet.providers   2.0.0      2023-10-17 [2] CRAN (R 4.3.2)
##  leafpop             0.1.0      2021-05-22 [2] CRAN (R 4.3.2)
##  lifecycle           1.0.4      2023-11-07 [2] CRAN (R 4.3.2)
##  listenv             0.9.0      2022-12-16 [2] CRAN (R 4.3.2)
##  lubridate         * 1.9.3      2023-09-27 [2] CRAN (R 4.3.2)
##  magrittr            2.0.3      2022-03-30 [2] CRAN (R 4.3.2)
##  maps                3.4.1.1    2023-11-03 [2] CRAN (R 4.3.2)
##  mapview           * 2.11.2     2023-10-13 [2] CRAN (R 4.3.2)
##  MASS                7.3-60     2023-05-04 [2] CRAN (R 4.3.2)
##  Matrix              1.6-1.1    2023-09-18 [2] CRAN (R 4.3.2)
##  memoise             2.0.1      2021-11-26 [2] CRAN (R 4.3.2)
##  mgcv                1.9-0      2023-07-11 [2] CRAN (R 4.3.2)
##  mime                0.12       2021-09-28 [2] CRAN (R 4.3.1)
##  mmod                1.3.3      2017-04-06 [2] CRAN (R 4.3.2)
##  munsell             0.5.0      2018-06-12 [2] CRAN (R 4.3.2)
##  mvtnorm             1.2-3      2023-08-25 [2] CRAN (R 4.3.2)
##  nlme                3.1-163    2023-08-09 [2] CRAN (R 4.3.2)
##  parallelly          1.36.0     2023-05-26 [2] CRAN (R 4.3.1)
##  patchwork         * 1.1.3      2023-08-14 [2] CRAN (R 4.3.2)
##  pegas               1.2        2023-02-23 [2] CRAN (R 4.3.2)
##  permute             0.9-7      2022-01-27 [2] CRAN (R 4.3.2)
##  pillar              1.9.0      2023-03-22 [2] CRAN (R 4.3.2)
##  pkgconfig           2.0.3      2019-09-22 [2] CRAN (R 4.3.2)
##  pkgdown             2.0.7      2022-12-14 [2] CRAN (R 4.3.2)
##  plyr                1.8.9      2023-10-02 [2] CRAN (R 4.3.2)
##  png                 0.1-8      2022-11-29 [2] CRAN (R 4.3.1)
##  polyclip            1.10-6     2023-09-27 [2] CRAN (R 4.3.1)
##  PopGenReport        3.1        2023-10-11 [2] CRAN (R 4.3.2)
##  prettycode          1.1.0      2019-12-16 [2] CRAN (R 4.3.2)
##  prettyunits         1.2.0      2023-09-24 [2] CRAN (R 4.3.2)
##  processx            3.8.2      2023-06-30 [2] CRAN (R 4.3.2)
##  progress            1.2.2      2019-05-16 [2] CRAN (R 4.3.2)
##  promises            1.2.1      2023-08-10 [2] CRAN (R 4.3.2)
##  proxy               0.4-27     2022-06-09 [2] CRAN (R 4.3.2)
##  ps                  1.7.5      2023-04-18 [2] CRAN (R 4.3.2)
##  purrr             * 1.0.2      2023-08-10 [2] CRAN (R 4.3.2)
##  quadprog            1.5-8      2019-11-20 [2] CRAN (R 4.3.1)
##  R.methodsS3         1.8.2      2022-06-13 [2] CRAN (R 4.3.1)
##  R.oo                1.25.0     2022-06-12 [2] CRAN (R 4.3.1)
##  R.utils             2.12.2     2022-11-11 [2] CRAN (R 4.3.2)
##  R6                  2.5.1      2021-08-19 [2] CRAN (R 4.3.2)
##  ragg                1.2.6      2023-10-10 [2] CRAN (R 4.3.2)
##  rappdirs            0.3.3      2021-01-31 [2] CRAN (R 4.3.2)
##  raster              3.6-26     2023-10-14 [2] CRAN (R 4.3.2)
##  rbibutils           2.2.16     2023-10-25 [2] CRAN (R 4.3.2)
##  RColorBrewer        1.1-3      2022-04-03 [2] CRAN (R 4.3.1)
##  Rcpp                1.0.11     2023-07-06 [2] CRAN (R 4.3.2)
##  Rdpack              2.6        2023-11-08 [2] CRAN (R 4.3.2)
##  readr             * 2.1.4      2023-02-10 [2] CRAN (R 4.3.2)
##  reshape             0.8.9      2022-04-12 [2] CRAN (R 4.3.2)
##  reshape2            1.4.4      2020-04-09 [2] CRAN (R 4.3.2)
##  reticulate          1.34.0     2023-10-12 [2] CRAN (R 4.3.2)
##  RgoogleMaps         1.5.1      2023-11-06 [2] CRAN (R 4.3.2)
##  rlang               1.1.2      2023-11-04 [2] CRAN (R 4.3.2)
##  rmarkdown           2.25       2023-09-18 [2] CRAN (R 4.3.2)
##  rprojroot           2.0.4      2023-11-05 [2] CRAN (R 4.3.2)
##  rstudioapi          0.15.0     2023-07-07 [2] CRAN (R 4.3.2)
##  sass                0.4.7      2023-07-15 [2] CRAN (R 4.3.2)
##  satellite           1.0.4      2021-10-12 [2] CRAN (R 4.3.2)
##  scales              1.2.1      2022-08-20 [2] CRAN (R 4.3.2)
##  seqinr              4.2-30     2023-04-05 [2] CRAN (R 4.3.2)
##  sessioninfo       * 1.2.2      2021-12-06 [2] CRAN (R 4.3.2)
##  sf                * 1.0-14     2023-07-11 [2] CRAN (R 4.3.2)
##  shiny               1.7.5.1    2023-10-14 [2] CRAN (R 4.3.2)
##  slimr             * 0.4.0.9000 2023-12-24 [1] local
##  SNPRelate         * 1.36.0     2023-10-24 [2] Bioconductor
##  sp                  2.1-1      2023-10-16 [2] CRAN (R 4.3.2)
##  spam                2.10-0     2023-10-23 [2] CRAN (R 4.3.2)
##  StAMPP              1.6.3      2021-08-08 [2] CRAN (R 4.3.2)
##  stringi             1.8.1      2023-11-13 [2] CRAN (R 4.3.2)
##  stringr           * 1.5.1      2023-11-14 [2] CRAN (R 4.3.2)
##  svglite             2.1.2      2023-10-11 [2] CRAN (R 4.3.2)
##  systemfonts         1.0.5      2023-10-09 [2] CRAN (R 4.3.2)
##  terra               1.7-55     2023-10-13 [2] CRAN (R 4.3.2)
##  textshaping         0.3.7      2023-10-09 [2] CRAN (R 4.3.2)
##  tibble            * 3.2.1      2023-03-20 [2] CRAN (R 4.3.2)
##  tidyr             * 1.3.0      2023-01-24 [2] CRAN (R 4.3.2)
##  tidyselect          1.2.0      2022-10-10 [2] CRAN (R 4.3.2)
##  timechange          0.2.0      2023-01-11 [2] CRAN (R 4.3.2)
##  tweenr              2.0.2      2022-09-06 [2] CRAN (R 4.3.2)
##  tzdb                0.4.0      2023-05-12 [2] CRAN (R 4.3.2)
##  units               0.8-4      2023-09-13 [2] CRAN (R 4.3.2)
##  utf8                1.2.4      2023-10-22 [2] CRAN (R 4.3.2)
##  uuid                1.1-1      2023-08-17 [2] CRAN (R 4.3.1)
##  vctrs               0.6.4      2023-10-12 [2] CRAN (R 4.3.2)
##  vegan               2.6-4      2022-10-11 [2] CRAN (R 4.3.2)
##  viridisLite         0.4.2      2023-05-02 [2] CRAN (R 4.3.2)
##  vroom               1.6.4      2023-10-02 [2] CRAN (R 4.3.2)
##  withr               2.5.2      2023-10-30 [2] CRAN (R 4.3.2)
##  xfun                0.41       2023-11-01 [2] CRAN (R 4.3.2)
##  xtable              1.8-4      2019-04-21 [2] CRAN (R 4.3.2)
##  yaml                2.3.7      2023-01-23 [2] CRAN (R 4.3.2)
##  zeallot             0.1.0      2018-01-28 [2] CRAN (R 4.3.2)
## 
##  [1] F:/Rtemp/Rtmp2rpzRH/temp_libpath4c983b7735dc
##  [2] C:/R/R-4.3.2/library
## 
## ──────────────────────────────────────────────────────────────────────────────