1 · What the app does
It assigns male and female broodstock to mating tanks so that, within each tank, the males and females are as unrelated as possible. That keeps inbreeding out of the next generation. Animals that don't fit are put in tank 0, which simply means "not used this time".
Relatedness comes from a genomic relationship matrix (GRM) built from SNP genotypes. The workflow is two steps:
genotypes.csv + sex.csv
│
▼ prepare_grm.R (R, on your computer)
grm_labelled.csv
│
▼ this web app (in your browser)
allocation CSV · closest-pairs CSV · PDF report
Privacy: the R script runs locally and the web app runs entirely inside your browser. No genotype or kinship data is ever uploaded anywhere, and the app works with the network disconnected.
2 · Step 1 — prepare your data in R
You need R with the rrBLUP package. Install it once:
install.packages("rrBLUP")
Input file 1 — genotypes.csv
One row per animal, one column per SNP. The first column is the animal ID;
the rest are genotypes coded 0, 1 or 2
(count of one allele). Missing values may be blank, NA, or any
number outside 0–2.
id,snp001,snp002,snp003,... BR0001,0,1,2,... BR0002,1,1,0,... BR0003,2,,1,... <- blank = missing
Input file 2 — sex.csv
Two columns named id and sex. Accepts
M/F, Male/Female, or
1/2 (1 = male, 2 = female).
Row order does not matter — animals are matched by ID.
id,sex BR0001,F BR0002,Male BR0003,1
Running it
Open prepare_grm.R, edit the SETTINGS block at the top so the
file names and output path match yours, then run:
Rscript prepare_grm.R
It prints a short report of what it did, for example:
read 240 animals x 150 SNPs from genotypes.csv QC: dropped 1 animal(s) below call rate 0.50, 2 without sex; 237 remain QC: dropped 3 SNP(s) below call rate 0.50; 147 remain GRM: 237 x 237 diagonal mean 0.991 off-diagonal range -0.42 .. 0.77 wrote grm_labelled.csv
What the quality control does
Only two filters are applied, both adjustable at the top of the script:
MIN_CALLRATE_SNP(default 0.5) — drops SNPs genotyped in too few animals.MIN_CALLRATE_IND(default 0.5) — drops animals with too few successful genotypes.
Animals without a usable sex are dropped too, because the app needs both sexes.
There is deliberately no minor-allele-frequency filter: rrBLUP::A.mat
produces an identical matrix with or without monomorphic markers, so it would
change nothing.
Sanity checks worth doing
- The reported diagonal mean should be close to 1. Much higher suggests inbred animals; far from 1 suggests a coding problem.
- Check the number of animals dropped is what you expect.
- Cross-check tag sex against your database before allocating — a mis-sexed animal quietly ruins a tank.
Already have a relationship matrix?
If you already have a GRM (or a pedigree-based A matrix) you can skip R entirely —
just format it as the labelled CSV described in the next section. The comment block
at the bottom of prepare_grm.R also shows how to feed in genotypes you
already hold in R (for example from an .Rdata file).
3 · Step 2 — load it into the app
The kinship file (required)
This is what prepare_grm.R writes. Header row is
id,sex, followed by every animal ID; each data row is the animal's ID,
its sex, then its relatedness to every animal in the same order as the header.
id,sex,BR0001,BR0002,BR0003 BR0001,F,0.9951,0.4327,-0.0212 BR0002,M,0.4327,1.0043,0.0155 BR0003,M,-0.0212,0.0155,0.9977
The app checks the file as it loads and refuses anything ambiguous — mismatched row/column order, an unrecognised sex, or a non-numeric cell (which would otherwise be silently treated as "completely unrelated" and produce a wrong allocation).
The constraints file (optional)
Only needed in Advanced mode. It is a separate file so you can change your
mind without re-running R. List only the animals you care about; unlisted animals are
unconstrained. Only id is required.
id,fix_tank,exclude,family BR0007,2,,F12 <- must go in tank 2; family F12 BR0019,,1,F12 <- must NOT be used (tank 0) BR0044,,,F12 <- free, but kept apart from other F12 animals
- fix_tank — pin an animal to a particular tank.
- exclude —
1keeps the animal out of every tank. - family — animals sharing a label are placed in different tanks. Use your full-sib family or pedigree group. This is the practical way to say "never put sibs together".
Constraints are never allowed to block a run. If they can't all be met — a family with more members than you have tanks, or more animals pinned to a tank than it holds — the app warns you first, then produces the best allocation it can and tells you exactly what it could not satisfy.
4 · Settings and what they mean
Simple or Advanced
Simple (the default) gives every tank the same capacity — set the number of tanks and how many males and females each holds. Advanced lets each tank have its own male and female counts, and adds the constraints file. Switching to Advanced starts from your Simple numbers, so nothing is lost; switching back warns before discarding per-tank differences. Both modes use the same optimiser.
| Setting | What it does | Guidance |
|---|---|---|
| Tanks | How many mating tanks to fill. | Your actual tank count. |
| Males / females per tank | Capacity of each tank. | Anything not placed goes to tank 0. Asking for more than you have is allowed — you'll be warned and tanks are under-filled. |
| Within-sex threshold | Same-sex pairs below this are ignored by the optimiser, so it concentrates on male–female relatedness. | 1 ignores all same-sex relatedness (matches the historical pipeline). 0.5 also keeps close same-sex kin apart. This is a breeding decision. |
| Restarts | How many independent attempts; the best is kept. | This is the knob that matters. 20 is a sensible default; raising it is cheap. |
| Iterations | Swap steps within each attempt. | Leave at 1200. The search settles after a couple of hundred steps, so more iterations achieve nothing — see the note below. |
| Seed | Makes the run repeatable. | The same seed and settings always reproduce the same allocation. Change it to explore alternatives. |
Why restarts rather than iterations? The search is a downhill one: it stops when no single swap improves things. Testing on this data showed a run settles after roughly 100–200 iterations and then never improves, so extra iterations are wasted effort. Independent restarts explore genuinely different starting points. At an equal compute budget, 20 restarts × 1000 iterations beat 1 restart × 20000.
5 · Reading the results
The headline number
Max opposite-sex kinship (any tank) is what the optimiser minimises: the single most-related male–female pair anywhere in your tanks. It is the fairest way to compare two runs. Lower is better; compare runs on this number, not on which animal ended up in which tank (the search is random, so membership shifts between seeds even when quality is the same).
There is no universal "good" value — it depends on how related your broodstock is. Compare against your own baseline: run once, note the number, and see whether more restarts or a different seed beats it.
Closest pairs to check
The most practical table on the page. It ranks the male–female pairs that ended up sharing a tank, most related first. Everything not on this list is less related than the entries shown, so if the top few are acceptable to you, the whole allocation is. Values at or above 0.25 (roughly a half-sib) are highlighted, and pairs sharing a family label are flagged.
Kinship after allocation
A heat map of every animal against every other, ordered by tank, with black lines at the tank boundaries. Squares on the diagonal are tanks. What you want is for the male–female blocks inside each tank to look "cool" (unrelated). Use the colour selector if you prefer a colour-blind-friendly or greyscale palette; the legend shows the scale, which is shared by every heat map so colours mean the same thing everywhere.
Within a single tank
One tank at a time with the animal IDs shown, males then females, split by a black line. The two blocks on the diagonal are male–male and female–female (expected to contain relatives — that's fine). The off-diagonal rectangle is the male–female block: that's the one that should be cool.
Kinship distributions
Three histograms on a shared scale:
- Overall — all pairs. Usually a hump near zero with a tail of relatives.
- Opposite-sex within tanks — should sit low with no tail. That tail being gone is the optimiser doing its job.
- Same-sex within tanks — keeps its tail, and that is correct. Same-sex relatives are left together deliberately; they don't mate, so they don't matter.
Constraint report
Shown when a constraints file was used, e.g. "3/3 pinned animals in their tank · 2/2 excluded kept out · 40 family conflicts unavoidable — tank 1: BR0001 + BR0002". Unavoidable conflicts are named so you can decide whether to accept them, add a tank, or relax a constraint.
6 · The files you can download, and what to use them for
| File | Use it for |
|---|---|
Allocation CSVMateAllocated_…csv |
The working document. Animal → tank number, so staff know which animal
goes where. Tank 0 = not used. Includes the constraint columns when
you supplied a constraints file. |
Closest pairs CSVClosestPairs_…csv |
The review list — hand it to whoever signs off the crossing plan. |
Sorted kinship matrix CSVGRM_…sorted_withID_…csv |
The full relatedness matrix reordered by tank, for your own analysis or to archive alongside the decision. |
PDF reportMateAllocation_report_…pdf |
The record. A summary sheet (settings, per-tank allocation, constraint outcome, conflicting pairs) followed by all the figures. Attach this to the breeding file. |
Reproducibility
Every file name carries the seed and date, and both CSVs begin with
# comment lines recording exactly what produced them — input files, tank
layout, threshold, restarts, iterations, seed, and the result achieved. Re-running with
the same settings and seed reproduces the same allocation.
Read one back in R without the comments getting in the way:
alloc <- read.csv("MateAllocated_....csv", comment.char = "#")
prov <- grep("^#", readLines("MateAllocated_....csv"), value = TRUE) # the settings
Excel shows the comment lines as ordinary text rows above the table.
7 · Troubleshooting
"Why are so many animals in tank 0?"
Tank 0 holds everyone who didn't fit. If you have 240 animals and ask for 5 tanks of
16+16, only 160 are placed — the other 80 land in tank 0. Increase the tanks or the
per-tank numbers to use more animals.
"I ran it twice and got different tanks."
Expected — the search is random. Fix the seed to repeat a run exactly, and
compare runs by the reported max kinship rather than by tank membership.
"My constraints weren't all satisfied."
Some sets are impossible: a family of 10 cannot be spread across 5 tanks without two
members sharing one. The app tells you up front, and the result names the conflicts.
Add tanks, split the family label, or accept the listed conflicts.
"The file won't load."
The message names the problem. Common causes: the header doesn't start with
id,sex,; the number of ID columns doesn't match the number of data rows;
a row's ID doesn't match the header in that position; or a cell is blank or
non-numeric. Re-export from prepare_grm.R to be sure of the format.
"The Cancel button is greyed out."
You opened the page directly from disk (file://), where the calculation
runs on the main thread and can't be interrupted. Serve the folder over HTTP instead
and Cancel works immediately:
cd webapp python3 -m http.server 8777 # then open http://localhost:8777
"Nothing happens / the page looks stale after an update."
Your browser cached the old files. Do a hard reload (Shift + reload).
"Is my data sent anywhere?"
No. Everything is computed in your browser; the page makes no network requests and
works offline. Nothing leaves your machine unless you download a file yourself.