R package · DuckDB-backed · MIT

Breeding simulation with a calendar, not a generation counter.

tidybreed is an R package for simulating animal and plant breeding programs. Every animal, locus and record lives in a DuckDB file on disk, so simulations scale past RAM, carry real dates, and bend to whatever your program actually looks like.

Current release
v0.70.0
License
MIT
Requirement
R ≥ 4.1
Meiosis kernel
C++17
tidybreed hex logo

The whole API is a pipe

Point at a table, filter to the animals you mean, act on them. That is every function in the package.

  1. 01

    Point at a table

    get_table() returns a lazy reference to any table in the database.

  2. 02

    Filter to the animals

    Standard dplyr filter(); the WHERE clause runs inside DuckDB.

  3. 03

    Act on them

    define_*() writes metadata, add_*() writes records on individuals.

  4. 04

    It is already saved

    New rows land in the .duckdb file. Nothing to collect, nothing to lose.

phenotype_boars.R
pop |>
  get_table("ind_meta") |>              # one row per animal
  filter(
    sex == "M",
    line_name == "Angus",
    off_test_date == current_date,      # your DATE column
    status == "on-test"                 # your VARCHAR column
  ) |>
  add_phenotype(
    c("adg", "backfat"),
    phenotype_date = current_date
  )

Why I started tidybreed

I struggled to learn the existing simulators, and when I did, I kept running into the same four walls.

None of them are bugs. They are design decisions that made sense for selection experiments and were inherited by everything since. tidybreed started as a clean sheet: what would a simulator look like if it were built for the breeding programs people actually run?

01
The wall

Generation is the clock

Most simulators advance the world one discrete generation at a time. Real programs do not: boars are used for eighteen months, cows calve across years, and young bulls compete with proven ones. Generation is an artifact of selection experiments, not of breeding programs.

tidybreed

Time is a column

tidybreed does not have a generation. Give an animal a birth date, a mating date, a cull date, and every event in the simulation happens on a calendar. If you want a generation column you can add one, but nothing forces it on you.

02
The wall

State lives in RAM

Genotypes, haplotypes and records accumulate in R objects generation after generation. Long-horizon or large-population runs simply run out of memory, so the design gets trimmed to fit the machine.

tidybreed

State lives on disk

Every table is in a DuckDB file. Queries are lazy and run inside the database, so a simulation can exceed available RAM, a run can be closed and reopened, and the file itself is the result you share.

03
The wall

The schema is closed

Want a herd, a production status, a cull reason or a test date on each animal? In most tools that means a second data frame you keep in sync by hand, and it breaks the moment you forget.

tidybreed

Any column, any table

Add columns of any type, or entire tables, with mutate_table() and define_table(). Descriptions travel with the database. Your simulation state is one place, queryable with dplyr, SQL or DBI.

04
The wall

A helper for everything

A function per mating scheme, per selection rule, per culling policy. The developer has to anticipate every design, and the day they did not, you are stuck writing around the package instead of with it.

tidybreed

You write the design

A mating plan is a tibble: one row per offspring you want, with sire, dam and sex. Selection is filter() and slice_max(). Nothing to learn that you do not already know from the tidyverse, and nothing to break when the package changes.

program_dates.R
# Declare the columns your program actually tracks
pop |>
  get_table("ind_meta") |>
  mutate_table(
    status        = NA_character_,   # VARCHAR
    birth_date    = as.Date(NA),     # DATE
    mate_date     = as.Date(NA),
    farrow_date   = as.Date(NA),
    wean_date     = as.Date(NA),
    off_test_date = as.Date(NA),
    cull_date     = as.Date(NA),
    alive         = TRUE,            # BOOLEAN, with a default
    .set_default  = TRUE
  )

# Later, on a given day, move boars off test
pop |>
  get_table("ind_meta") |>
  filter(sex == "M", off_test_date == today) |>
  mutate_table(status = "after-test-boar")

The main reason: dates

A DATE column in a database is the whole difference between a selection experiment and a breeding program.

DuckDB gives tidybreed real DATE and TIMESTAMP types, and mutate_table() lets you put them on any table. Once an animal has a birth date and a set of event dates, the simulation stops being a sequence of generations and becomes a calendar: matings happen in a season, animals come off test on a day, culls happen when a rule fires.

That is what makes the outputs realistic. Genetic gain and inbreeding come out per year against the true birth dates, on overlapping generations, exactly as they are reported in practice. Overlap, generation interval and age structure fall out of the data instead of being assumed.

ΔG / yr
gain against birth dates, not generation number
ΔF / yr
inbreeding on the real age structure
L
generation interval measured, not assumed

Who it is for

Two audiences, one database. The same file serves a company forecasting next year and a lab testing a genome nobody has simulated before.

For industry

Numbers you can report

A breeding company does not report genetic gain per generation. It reports it per year, on a program with overlapping generations, seasonal matings and animals that leave for reasons other than selection.

  • ΔG and ΔF per calendar year

    Because every animal carries dates, you compute gain and inbreeding per year straight from the tables, the way you already present them to management.

  • A digital twin of the flow

    Status columns move animals through on-test, off-test, active and culled. Filter on them the same way you would query your own database.

  • Scenarios and replicates

    Parameterise runs from YAML, stamp a replicate number on each, and merge them into one archive file with archive_replicate(). Built for HPC array jobs.

  • Your evaluation, not a stand-in

    Write out to BLUPF90, JWAS or PLINK folders, run the evaluation you actually use, and read the solutions back with add_ebv().

For academia

Room to be unusual

Research questions rarely fit the default genome. tidybreed keeps the biology in explicit tables you can inspect and override, and it never stops you from writing your own step in the middle of a pipeline.

  • Sex chromosomes and organelles

    X/Y, Z/W, X0 and mitochondria are rows in chr_inheritance and chr_recombination, set per chromosome with define_chromosome(). Sex- and line-specific genetic maps are more rows, not a new schema.

  • Six founder haplotype methods

    Uniform, fixed, Beta and Balding–Nichols frequencies without LD; Li–Stephens mosaic and Gaussian copula with LD along the map.

  • Inject your own solution anywhere

    Tables are SQL. Compute EBVs in your own solver, write them to ind_ebv, and carry on. DBI gives you the connection whenever a function does not exist yet.

  • Reproducible by construction

    The .duckdb file is the state. Share it, reopen it with restore_pop(), or open it from Python or the DuckDB CLI without R at all.

Try it

Install from GitHub with pak. You need a C++ compiler; the install tells you exactly which one if it is missing.

Status

tidybreed is in alpha. The API is settling toward 1.0 and pre-1.0 releases can break things, so pin the version you build on. I am actively looking for feedback on the design: if you simulate breeding programs and something here does not fit how you work, I want to hear it.

install.R
install.packages("pak")
pak::pak("austin-putz/tidybreed")
library(tidybreed)

# Pre-1.0: pin the version you build on
pak::pak("austin-putz/tidybreed@v0.70.0")
Also on this site: tidybreed is listed with the other simulation software. The R, DuckDB, tidyverse and C++ marks belong to their respective projects and are used here only to describe what tidybreed is built on.