diff --git a/R/Host.R b/R/Host.R index b2bda7e..7d334f7 100644 --- a/R/Host.R +++ b/R/Host.R @@ -82,6 +82,75 @@ Host <- R6Class( private$pathogens[[length(private$pathogens)+1]] <- new.pathogen }, + # --- fixed-size lineage pool (for recombination parent sampling) --- + # Fixes exponential lineage growth under recombination: instead of + # always creating a brand-new ancestral lineage, recombination should + # sample a parent from a FIXED pool of n (= population size) possible + # lineages, only "activating" a previously-inactive one when chosen. + # Total pool size never changes; only how many slots are active does. + # + # Only ACTIVE slots are stored explicitly (a small, variable-size + # collection). Inactive slots are all interchangeable/anonymous until + # touched, so they're never materialized -- just counted as + # pool.size - n.active. New slot ids are generated on demand rather + # than pre-numbered 1..n. + init.pool = function(n) { + if (is.null(private$pool.size)) { + private$pool.size <- n + private$active.occupants <- list() + private$next.slot.id <- 0L + } + }, + get.pool.size = function() { private$pool.size }, + is.pool.initialized = function() { !is.null(private$pool.size) }, + count.active.slots = function() { length(private$active.occupants) }, + count.inactive.slots = function() { + private$pool.size - length(private$active.occupants) + }, + is.slot.active = function(slot.id) { + as.character(slot.id) %in% names(private$active.occupants) + }, + get.slot.occupant = function(slot.id) { + private$active.occupants[[as.character(slot.id)]] + }, + get.active.slot.ids = function() { + as.integer(names(private$active.occupants)) + }, + # activate a genuinely NEW (never-before-used) slot with a fresh id + activate.new.slot = function(pathogen) { + private$next.slot.id <- private$next.slot.id + 1L + new.id <- private$next.slot.id + pathogen$set.slot.id(new.id) + private$active.occupants[[as.character(new.id)]] <- pathogen + new.id + }, + # re-activate an EXISTING slot id with a (possibly different) occupant + # -- used when a slot's occupant changes (e.g. after coalescence) + activate.slot = function(slot.id, pathogen) { + private$active.occupants[[as.character(slot.id)]] <- pathogen + }, + deactivate.slot = function(slot.id) { + private$active.occupants[[as.character(slot.id)]] <- NULL + }, + # sample a recombination "other parent" from the fixed pool, uniformly + # over all pool.size slots (optionally excluding one, e.g. the child's + # own slot). Returns a list(active=TRUE/FALSE, pathogen=) -- caller creates+activates a new lineage when active=FALSE. + sample.other.slot = function(exclude.slot.id=NA) { + active.ids <- names(private$active.occupants) + if (!is.na(exclude.slot.id)) { + active.ids <- setdiff(active.ids, as.character(exclude.slot.id)) + } + n.total <- private$pool.size - (if (is.na(exclude.slot.id)) 0L else 1L) + if (n.total <= 0) return(list(active=FALSE, pathogen=NULL)) + n.active <- length(active.ids) + if (runif(1) < n.active / n.total) { + chosen.id <- if (n.active == 1) active.ids else sample(active.ids, 1) + return(list(active=TRUE, pathogen=private$active.occupants[[chosen.id]])) + } + list(active=FALSE, pathogen=NULL) + }, + remove.pathogen = function(idx) { path <- private$pathogens[[idx]] private$pathogens[[idx]] <- NULL @@ -136,7 +205,10 @@ Host <- R6Class( sampling.time = NULL, sampling.comp = NULL, unsampled = NULL, - pathogens = NULL + pathogens = NULL, + active.occupants = NULL, + pool.size = NULL, + next.slot.id = NULL ) ) diff --git a/R/Model.R b/R/Model.R index 7ef2b1d..a73646b 100644 --- a/R/Model.R +++ b/R/Model.R @@ -328,6 +328,19 @@ Model <- R6Class( } private$check.expression(params$pop.size, env) private$pop.sizes[[src]] <- params$pop.size + } else if (!is.null(params$coalescent.rate) && + params$coalescent.rate != "Inf") { + # coalescence is enabled for this compartment (finite rate) but + # pop.size was never explicitly set, so it's silently using the + # unconfigured default (100) baked into this package -- this + # default has no connection to the compartment's own `size:` + # field or any other yaml value, and coalescent.rate expressions + # that reference population size (or were chosen assuming a + # particular population size) may be silently mismatched with it. + warning(src, ": coalescent.rate is set but pop.size is not -- ", + "using unconfigured default pop.size=100. If ", + "coalescent.rate was chosen with a particular population ", + "size in mind, set pop.size explicitly in the yaml.") } } # end loop diff --git a/R/Pathogen.R b/R/Pathogen.R index b81f3b7..67ebcde 100644 --- a/R/Pathogen.R +++ b/R/Pathogen.R @@ -41,6 +41,8 @@ Pathogen <- R6Class( # immutable attributes get.name = function() { private$name }, get.end.time = function() { private$end.time }, + get.slot.id = function() { private$slot.id }, + set.slot.id = function(id) { private$slot.id <- id }, # mutables get.start.time = function() { private$start.time }, @@ -78,6 +80,7 @@ Pathogen <- R6Class( end.time = NULL, parents = NULL, children = NULL, - breakpoint = NULL + breakpoint = NULL, + slot.id = NA ) ) diff --git a/R/simARG.R b/R/simARG.R index 251df9e..05ee944 100644 --- a/R/simARG.R +++ b/R/simARG.R @@ -63,8 +63,11 @@ sim.arg <- function(outer, rho = 1e-4, seq.length = 9000L) { if (ev$type == "coalescent") { .do.coalescent(ev$host, inner, event.time, envir = env) } else { + host.obj <- active$get.host.by.name(ev$host) + p.size.expr <- mod$get.pop.size(host.obj$get.compartment()) + p.size.val <- eval(parse(text = p.size.expr), envir = env) bp <- .do.recombination(ev$host, ev$pathogen, inner, event.time, - seq.length = seq.length) + seq.length = seq.length, p.size = p.size.val) breakpoints[[bp$child]] <- bp$position } } @@ -133,6 +136,7 @@ sim.arg <- function(outer, rho = 1e-4, seq.length = 9000L) { # coalescence rate for this host (requires 2+ lineages) if (k >= 2) { + expr <- mod$get.coalescent.rate(comp) rate <- eval(parse(text = expr), envir = envir) if (rate > 0) { @@ -190,20 +194,52 @@ sim.arg <- function(outer, rho = 1e-4, seq.length = 9000L) { #' @keywords internal #' @noRd .do.recombination <- function(host.name, pathogen, inner, time, - seq.length = 9000L) { + seq.length = 9000L, p.size = NULL) { active <- inner$get.active() host <- active$get.host.by.name(host.name) + # initialize this host's FIXED lineage pool (sample + # recombination parents from a fixed pool of p.size lineages, only + # activating a previously-inactive one when chosen, instead of always + # creating a brand-new lineage de novo. Total pool size never changes.) + if (!host$is.pool.initialized()) { + if (is.null(p.size)) { + stop(".do.recombination: p.size must be provided to initialize ", + "host's lineage pool on first use") + } + host$init.pool(p.size) + } + + # ensure the recombining pathogen already occupies a pool slot -- if + # this is the first pool-tracked event involving it, assign one now + if (is.na(pathogen$get.slot.id())) { + host$activate.new.slot(pathogen) + } + own.slot <- pathogen$get.slot.id() + # sample breakpoint uniformly across genome breakpoint <- sample.int(seq.length - 1L, 1L) pathogen$set.breakpoint(breakpoint) - - # end the current lineage at this recombination event pathogen$set.start.time(time) - # create two parental lineages — left and right of breakpoint - parent.left <- inner$new.pathogen(time) - parent.right <- inner$new.pathogen(time) + # LEFT parent: continues in the SAME slot as the child + parent.left <- inner$new.pathogen(time) + parent.left$set.slot.id(own.slot) + host$activate.slot(own.slot, parent.left) + + # RIGHT parent: sample from the fixed pool (excluding own slot) + if (host$get.pool.size() <= 1) { + # degenerate case: pool size 1, nothing else to sample from + parent.right <- parent.left + } else { + draw <- host$sample.other.slot(exclude.slot.id = own.slot) + if (draw$active) { + parent.right <- draw$pathogen + } else { + parent.right <- inner$new.pathogen(time) + host$activate.new.slot(parent.right) + } + } # record parent-child relationships (recombination has two parents) parent.left$add.child(pathogen) @@ -216,9 +252,12 @@ sim.arg <- function(outer, rho = 1e-4, seq.length = 9000L) { idx <- which(sapply(paths, function(p) p$get.name()) == pathogen$get.name()) if (length(idx) == 1) host$remove.pathogen(idx) host$add.pathogen(parent.left) - host$add.pathogen(parent.right) + already.present <- any(sapply(host$get.pathogens(), function(p) { + p$get.name() == parent.right$get.name() + })) + if (!already.present) host$add.pathogen(parent.right) - # log the recombination event (breakpoint not stored in log — fixed schema) + # log the recombination event (breakpoint not stored in log -- fixed schema) event <- list( time = time, event = "recombination", from.comp = host$get.compartment(), to.comp = NA, @@ -252,7 +291,11 @@ sim.arg <- function(outer, rho = 1e-4, seq.length = 9000L) { #' @export resolve.arg <- function(arg.result, seq.length = 9000L) { inner <- arg.result$inner - bps <- sort(unique(unlist(arg.result$breakpoints))) + # bp.by.child keeps names (per-child lookup); bps is deduped positions + # only, for segment boundaries -- unique() strips names, so don't use + # bps for per-child lookup + bp.by.child <- unlist(arg.result$breakpoints) + bps <- sort(unique(bp.by.child)) log <- inner$get.log() log$time <- as.numeric(log$time) @@ -276,48 +319,81 @@ resolve.arg <- function(arg.result, seq.length = 9000L) { ) # for each segment, trace from tips to root + # precompute everything that does NOT depend on segment position -- + # these were being recomputed fresh inside the per-segment loop below + # (unvectorized trans.rows loop, re-filtering recomb.rows per child per + # segment) even though none of it references pos. With thousands of + # segments this was the dominant cost (confirmed via Rprof: [.data.frame + # subsetting was ~66% of total resolve.arg time). Only the recombination + # parent CHOICE (left vs right) genuinely depends on pos. + base.parent.map <- setNames(coal.rows$pathogen1, coal.rows$pathogen2) + trans.p1 <- trans.rows$pathogen1 + trans.p2 <- trans.rows$pathogen2 + trans.valid <- !is.na(trans.p1) & !is.na(trans.p2) + base.parent.map[trans.p2[trans.valid]] <- trans.p1[trans.valid] + + recomb.by.child <- split(recomb.rows, recomb.rows$pathogen1) + recomb.children <- names(recomb.by.child) + local.trees <- vector("list", length(starts)) for (i in seq_along(starts)) { pos <- (starts[i] + ends[i]) / 2 - # parent map for this segment - parent.map <- setNames(coal.rows$pathogen1, coal.rows$pathogen2) - for (r in seq_len(nrow(trans.rows))) { - p2 <- trans.rows$pathogen2[r] - p1 <- trans.rows$pathogen1[r] - if (!is.na(p2) && !is.na(p1)) parent.map[p2] <- p1 - } - recomb.children <- unique(recomb.rows$pathogen1) + parent.map <- base.parent.map for (child in recomb.children) { - child.rows <- recomb.rows[recomb.rows$pathogen1 == child, ] + child.rows <- recomb.by.child[[child]] if (nrow(child.rows) < 2) next - bp <- bps[names(bps) == child] - if (length(bp) == 0) bp <- bps[1] + bp <- bp.by.child[child] + if (length(bp) == 0 || is.na(bp)) bp <- bps[1] parent <- if (pos <= bp) child.rows$pathogen2[1] else child.rows$pathogen2[2] parent.map[child] <- parent } - # trace tips to root - all.nodes <- character(0) - edges <- data.frame(parent=character(0), child=character(0), - stringsAsFactors=FALSE) + # trace tips to root -- preallocate buffers and fill by index rather + # than growing vectors with c() (still O(n^2) even without rbind/ + # data.frame -- each c() call still copies the whole vector so far). + # A chain from any tip can't revisit a node (monotonic trace upward), + # so length(all.paths) is a safe upper bound on any single chain's + # length; multiply by n.tips for a safe total upper bound, then + # truncate to what was actually used. + max.steps <- length(all.paths) * length(tips) + edge.parents <- character(max.steps) + edge.children <- character(max.steps) + all.nodes.buf <- character(max.steps * 2L + length(tips)) + n.edges <- 0L + n.nodes <- 0L for (tip in tips) { cur <- tip while (!is.na(parent.map[cur]) && !is.null(parent.map[cur])) { par <- parent.map[cur] - edges <- rbind(edges, data.frame(parent=par, child=cur, - stringsAsFactors=FALSE)) - all.nodes <- c(all.nodes, cur, par) + n.edges <- n.edges + 1L + edge.parents[n.edges] <- par + edge.children[n.edges] <- cur + n.nodes <- n.nodes + 1L; all.nodes.buf[n.nodes] <- cur + n.nodes <- n.nodes + 1L; all.nodes.buf[n.nodes] <- par cur <- par } - all.nodes <- c(all.nodes, cur) + n.nodes <- n.nodes + 1L + all.nodes.buf[n.nodes] <- cur } + edges <- data.frame(parent=edge.parents[seq_len(n.edges)], + child=edge.children[seq_len(n.edges)], + stringsAsFactors=FALSE) + all.nodes <- all.nodes.buf[seq_len(n.nodes)] all.nodes <- unique(all.nodes) edges <- unique(edges) root.node <- all.nodes[!all.nodes %in% edges$child] if (length(root.node) > 1) root.node <- root.node[1] - get.children <- function(node) edges$child[edges$parent == node] + # O(1) child lookup via precomputed split, instead of scanning the + # full edges data.frame on every call -- to.newick() below calls + # this once per node in the tree, so the previous O(n) scan made + # the whole traversal O(n^2) (confirmed dominant cost via Rprof). + children.map <- split(edges$child, edges$parent) + get.children <- function(node) { + ch <- children.map[[node]] + if (is.null(ch)) character(0) else ch + } get.time <- function(node) { t <- node.created[node] diff --git a/R/simInnerTree.R b/R/simInnerTree.R index e5c9cd7..a02fc70 100644 --- a/R/simInnerTree.R +++ b/R/simInnerTree.R @@ -202,10 +202,37 @@ sim.inner.tree <- function(outer) { expr <- inner$get.model()$get.pop.size(e$to.comp) p.size <- eval(parse(text=expr), envir=envir) + if (count > p.size) { + # count (n.active.lineages) is tracked ancestral segment/lineage + # objects, not necessarily distinct physical genomes -- once + # recombination is active, lineage count can legitimately exceed + # the population's census size, and the correct relationship + # between the two is not yet resolved (see issue tracker: rhyper() + # here implicitly assumes count occupies count distinct slots + # among p.size exchangeable individuals, which breaks once one + # genome can carry several ancestral segments). Fail loudly with + # a clear diagnostic rather than silently capping or crashing on + # an opaque NA, until the bottleneck/occupancy model is revisited. + stop(sprintf( + "Tracked lineage count (%d) exceeds pathogen population size (%d) ", + count, p.size), + "in host ", recipient$get.name(), " at time ", e$time, ". ", + "This means more ancestral lineages/segments are being tracked ", + "than the model's nominal population size anticipates (likely ", + "from recombination). The bottleneck sampling here assumes ", + "count occupies count distinct slots among p.size individuals, ", + "which is not valid once lineage count and physical individual ", + "count can diverge -- needs a proper occupancy/carrier model, ", + "not a silent cap.") + } n.transfer <- rhyper(1, count, p.size-count, b.size) if (n.transfer > 0) { for (i in 1:n.transfer) { path <- recipient$remove.pathogen(1) + if (!is.na(path$get.slot.id()) && recipient$is.pool.initialized()) { + recipient$deactivate.slot(path$get.slot.id()) + } + path$set.slot.id(NA) source$add.pathogen(path) event$pathogen1 <- path$get.name() inner$add.event(event) @@ -233,6 +260,18 @@ sim.inner.tree <- function(outer) { if (count > 0) { for (i in 1:count) { path <- recipient$remove.pathogen(1) + # backward in time, this pathogen's old slot in recipient + # must be freed -- otherwise repeated superinfection transfers + # leave the pool bookkeeping stale (slots marked active forever + # even though their occupant left), eventually starving future + # recombination events of any slot that correctly registers as + # free. Reset the pathogen's own slot.id too, since slot ids are + # host-local -- it gets a fresh one lazily if/when it's ever + # involved in a recombination event in its NEW host. + if (!is.na(path$get.slot.id()) && recipient$is.pool.initialized()) { + recipient$deactivate.slot(path$get.slot.id()) + } + path$set.slot.id(NA) source$add.pathogen(path) event$pathogen1 <- path$get.name() # copy-on-modify inner$add.event(event) @@ -321,6 +360,27 @@ sim.inner.tree <- function(outer) { p2$set.start.time(time) anc <- inner$new.pathogen(time) # sets end.time + + # reconcile the lineage pool: the ancestor represents the same + # physical individual as whichever of p1/p2 already occupied a pool + # slot (from a prior recombination event). If both occupied slots, + # keep one for the ancestor and free the other -- two active + # lineages coalescing means one fewer active individual going + # forward. If neither occupied a slot, this coalescence never + # touched the pool, so the ancestor stays unassigned too. + p1.slot <- p1$get.slot.id() + p2.slot <- p2$get.slot.id() + if (!is.na(p1.slot)) { + anc$set.slot.id(p1.slot) + host$activate.slot(p1.slot, anc) + if (!is.na(p2.slot) && p2.slot != p1.slot) { + host$deactivate.slot(p2.slot) + } + } else if (!is.na(p2.slot)) { + anc$set.slot.id(p2.slot) + host$activate.slot(p2.slot, anc) + } + host$add.pathogen(anc) # assign ancestral/descendant relations diff --git a/R/simOuterTree.R b/R/simOuterTree.R index 9ebcca6..3f19fdf 100644 --- a/R/simOuterTree.R +++ b/R/simOuterTree.R @@ -223,6 +223,14 @@ sim.outer.tree <- function(dynamics) { } stopifnot(n.source > 0) n.active.source <- active$count.type(e$source) + if (e$source == e$to.comp && is.superinfect) { + # superinfection keeps recip active (not removed above), so it's + # still counted here -- but n.source already excluded it via the + # -1 adjustment. Exclude it consistently here too, or the active + # count can exceed the aggregate count by exactly 1 whenever + # source and recipient compartments match under superinfection. + n.active.source <- n.active.source - 1 + } stopifnot(n.active.source <= n.source) if (sample(1:n.source, 1) <= n.active.source) { diff --git a/tests/testthat/test_Superinfection.yaml b/tests/testthat/test_Superinfection.yaml index fc96ce3..1f2db63 100644 --- a/tests/testthat/test_Superinfection.yaml +++ b/tests/testthat/test_Superinfection.yaml @@ -24,6 +24,7 @@ Compartments: size: 2 bottleneck.size: 1 coalescent.rate: 0.01 + pop.size: 100 # was silently defaulting to this I_samp: infected: true size: 0 diff --git a/tests/testthat/test_simARG.R b/tests/testthat/test_simARG.R index c049d5b..a35c8d9 100644 --- a/tests/testthat/test_simARG.R +++ b/tests/testthat/test_simARG.R @@ -207,3 +207,116 @@ test_that("resolve.arg produces genuinely divergent topology (hand-built positiv expect_true(is.monophyletic(phy2, c("A","C"))) expect_false(is.monophyletic(phy2, c("A","B"))) }) +test_that("Host lineage pool: basic activate/deactivate/query", { + h <- Host$new(name="H1", compartment="I") + expect_false(h$is.pool.initialized()) + h$init.pool(5) + expect_true(h$is.pool.initialized()) + expect_equal(h$get.pool.size(), 5) + expect_equal(h$count.inactive.slots(), 5) + expect_equal(h$count.active.slots(), 0) + + p <- Pathogen$new(name="P1", end.time=1) + new.id <- h$activate.new.slot(p) + expect_equal(p$get.slot.id(), new.id) + expect_true(h$is.slot.active(new.id)) + expect_equal(h$get.slot.occupant(new.id)$get.name(), "P1") + expect_equal(h$count.active.slots(), 1) + expect_equal(h$count.inactive.slots(), 4) + + h$deactivate.slot(new.id) + expect_false(h$is.slot.active(new.id)) + expect_equal(h$count.active.slots(), 0) + # pool size must not shrink after deactivation + expect_equal(h$get.pool.size(), 5) +}) + +test_that("Host lineage pool: init.pool is idempotent", { + h <- Host$new(name="H1", compartment="I") + h$init.pool(10) + h$init.pool(999) # should be ignored, pool already initialized + expect_equal(h$get.pool.size(), 10) +}) + +test_that("sim.arg does not exceed pop.size at high rho (fixed lineage pool)", { + settings <- read_yaml("test_Superinfection.yaml") + settings$Parameters$sigma <- 0.05 + mod <- Model$new(settings) + set.seed(33) + dyn <- tryCatch(sim.dynamics(mod, max.attempts=10), error=function(e) NULL) + if (is.null(dyn)) skip("could not build dynamics for this seed") + outer <- tryCatch( + withCallingHandlers(sim.outer.tree(dyn), warning=function(w) invokeRestart("muffleWarning")), + error=function(e) NULL) + if (is.null(outer)) skip("could not build outer tree for this seed") + + # rho=5,7,10 previously exceeded pop.size and crashed/errored before + # the fixed lineage pool (Art's design: sample recombination parents + # from a fixed pool of p.size lineages instead of always creating a + # new lineage de novo). Now they should all succeed. + for (rho in c(5, 7, 10)) { + arg <- tryCatch(sim.arg(outer, rho=rho, seq.length=9000), error=function(e) e) + expect_false(inherits(arg, "error"), + info=paste("rho =", rho, "should not error with fixed lineage pool")) + } +}) + +test_that("resolve.arg produces valid trees on top of the fixed lineage pool", { + settings <- read_yaml("test_Superinfection.yaml") + settings$Parameters$sigma <- 0.05 + mod <- Model$new(settings) + set.seed(33) + dyn <- tryCatch(sim.dynamics(mod, max.attempts=10), error=function(e) NULL) + if (is.null(dyn)) skip("could not build dynamics for this seed") + outer <- tryCatch( + withCallingHandlers(sim.outer.tree(dyn), warning=function(w) invokeRestart("muffleWarning")), + error=function(e) NULL) + if (is.null(outer)) skip("could not build outer tree for this seed") + + n.sampled <- outer$get.sampled()$count.type() + arg <- sim.arg(outer, rho=2, seq.length=9000) + res <- resolve.arg(arg, seq.length=9000) + + valid <- sapply(res$local.trees, function(lt) { + phy <- lt$phylo + inherits(phy, "phylo") && + length(phy$tip.label) == n.sampled && + sum(duplicated(phy$tip.label)) == 0 && + !any(is.na(phy$edge.length)) && + !any(phy$edge.length < 0, na.rm=TRUE) + }) + expect_true(all(valid)) +}) +test_that("resolve.arg: per-child breakpoint lookup is correct with multiple distinct breakpoints", { + # A and C recombine independently at different breakpoints (300, 700) + times <- c(A=10,B=10,C=10,D=10, L1=8,R1=8, L2=7,R2=7, + AB=5,CD=5, RR=4, ABCD=2, ROOT=1) + paths <- lapply(names(times), function(n) Pathogen$new(name=n, end.time=times[[n]])) + names(paths) <- names(times) + log <- data.frame( + time = c(10,10,10,10, 8,8, 7,7, 5,5, 5,5, 4,4, 2,2, 1,1), + event = c(rep("sampling",4), rep("recombination",2), rep("recombination",2), + rep("coalescent",2), rep("coalescent",2), rep("coalescent",2), + rep("coalescent",2), rep("coalescent",2)), + pathogen1 = c("A","B","C","D", "A","A", "C","C", "AB","AB", "CD","CD", + "RR","RR", "ABCD","ABCD", "ROOT","ROOT"), + pathogen2 = c(NA,NA,NA,NA, "L1","R1", "L2","R2", "L1","B", "L2","D", + "R1","R2", "AB","CD", "ABCD","RR"), + stringsAsFactors=FALSE + ) + fake.inner <- list(get.log=function() log, get.all.pathogens=function() paths) + arg.result <- list(inner=fake.inner, breakpoints=list(A=300L, C=700L)) + res <- resolve.arg(arg.result, seq.length=1000L) + + expect_equal(length(res$local.trees), 3) + + phy1 <- collapse.singles(res$local.trees[[1]]$phylo) + phy2 <- collapse.singles(res$local.trees[[2]]$phylo) + phy3 <- collapse.singles(res$local.trees[[3]]$phylo) + + expect_true(is.monophyletic(phy1, c("C","D"))) + expect_true(is.monophyletic(phy2, c("C","D"))) + expect_false(is.monophyletic(phy2, c("A","C"))) + expect_true(is.monophyletic(phy3, c("A","C"))) + expect_false(is.monophyletic(phy3, c("C","D"))) +}) diff --git a/tests/testthat/test_simInnerTree.R b/tests/testthat/test_simInnerTree.R index e6c147e..22e6880 100644 --- a/tests/testthat/test_simInnerTree.R +++ b/tests/testthat/test_simInnerTree.R @@ -3,6 +3,7 @@ require(twt) # generate test fixtures settings <- yaml.load_file("test_SIR.yaml") settings$Compartments$I$coalescent.rate <- 1.0 +settings$Compartments$I$pop.size <- 100 # was silently defaulting to this mod <- Model$new(settings) set.seed(276) dynamics <- sim.dynamics(mod) diff --git a/tests/testthat/test_superinfection.R b/tests/testthat/test_superinfection.R index 4d79ceb..6662525 100644 --- a/tests/testthat/test_superinfection.R +++ b/tests/testthat/test_superinfection.R @@ -393,7 +393,7 @@ test_that("model: compartment I flagged infected=TRUE", { expect_true(mod$get.infected("I")) }) -test_that("model YAML: compartment I has pop.size=2 and bottleneck.size=1", { +test_that("model YAML: compartment I has size=2 and bottleneck.size=1", { try_model(SUPERINF_INNER_PATH) cfg <- read_yaml(SUPERINF_INNER_PATH) expect_equal(cfg$Compartments$I$size, 2)