using GerbilManagerWebAPI.Genetics; namespace GerbilManager.Tests { /// /// Exact-math checks for the inbreeding coefficient (Inzuchtkoeffizient) against /// the textbook reference values for canonical pedigrees. /// public class PedigreeCalculatorTests { private const int Precision = 10; [Fact] public void UnknownAncestry_IsZero() { var ped = new PedigreeBuilder(); var lonely = ped.Add("Lonely"); var calc = ped.Build(); var result = calc.ForIndividual(lonely); Assert.Equal(0.0, result.Coefficient, Precision); Assert.Equal(0, result.GenerationsAvailable); Assert.Empty(result.CommonAncestors); } [Fact] public void OneKnownParent_IsZero() { var ped = new PedigreeBuilder(); var sire = ped.Add("Sire"); var calc = ped.Build(); // Only one parent known → no inbreeding can be established. var result = calc.ForOffspringOf(sire, null); Assert.Equal(0.0, result.Coefficient, Precision); Assert.Empty(result.CommonAncestors); } [Fact] public void ParentOffspring_IsQuarter() { // Z is the offspring of P mated with its own daughter C. var ped = new PedigreeBuilder(); var p = ped.Add("P"); var mate = ped.Add("Mate"); var c = ped.AddChild("C", p, mate); var calc = ped.Build(); var result = calc.ForOffspringOf(p, c); Assert.Equal(0.25, result.Coefficient, Precision); Assert.Equal(25.0, result.Percent, Precision); var ancestor = Assert.Single(result.CommonAncestors); Assert.Equal(p, ancestor.Id); Assert.Equal(0.25, ancestor.Contribution, Precision); } [Fact] public void FullSiblings_IsQuarter() { // Parents S and D are full siblings (both children of GF and GM). var ped = new PedigreeBuilder(); var gf = ped.Add("GF"); var gm = ped.Add("GM"); var s = ped.AddChild("S", gf, gm); var d = ped.AddChild("D", gf, gm); var calc = ped.Build(); var result = calc.ForOffspringOf(s, d); Assert.Equal(0.25, result.Coefficient, Precision); Assert.Equal(2, result.CommonAncestors.Count); Assert.All(result.CommonAncestors, a => Assert.Equal(0.125, a.Contribution, Precision)); } [Fact] public void HalfSiblings_IsEighth() { // Parents S and D share only GF (other parents unrelated). var ped = new PedigreeBuilder(); var gf = ped.Add("GF"); var x = ped.Add("X"); var y = ped.Add("Y"); var s = ped.AddChild("S", gf, x); var d = ped.AddChild("D", gf, y); var calc = ped.Build(); var result = calc.ForOffspringOf(s, d); Assert.Equal(0.125, result.Coefficient, Precision); var ancestor = Assert.Single(result.CommonAncestors); Assert.Equal(gf, ancestor.Id); Assert.Equal(0.125, ancestor.Contribution, Precision); } [Fact] public void FirstCousins_IsSixteenth() { // Parents S and D are first cousins: their respective parents A1 and B1 // are full siblings (children of GF and GM). var ped = new PedigreeBuilder(); var gf = ped.Add("GF"); var gm = ped.Add("GM"); var a1 = ped.AddChild("A1", gf, gm); var b1 = ped.AddChild("B1", gf, gm); var x = ped.Add("X"); var y = ped.Add("Y"); var s = ped.AddChild("S", a1, x); var d = ped.AddChild("D", b1, y); var calc = ped.Build(); var result = calc.ForOffspringOf(s, d); Assert.Equal(0.0625, result.Coefficient, Precision); Assert.Equal(2, result.CommonAncestors.Count); Assert.All(result.CommonAncestors, a => Assert.Equal(0.03125, a.Contribution, Precision)); } [Fact] public void DeepCommonAncestry_SumsAllDisjointPaths() { // Full siblings S and D whose shared parents (GF, GM) are themselves full // siblings (children of GGF, GGM). GF and GM are NOT inbred (their parents // are unrelated founders, so F_GF = F_GM = 0), but the great-grandparents // are reached by additional disjoint paths: // GF + GM: 2 * (1/2)^3 = 0.25 // GGF: 2 * (1/2)^5 = 0.0625 (S-GF-GGF / D-GM-GGF and swap) // GGM: 2 * (1/2)^5 = 0.0625 // --------------------------------------------- // F = 0.375 (verified against the recursive kinship method) var ped = new PedigreeBuilder(); var ggf = ped.Add("GGF"); var ggm = ped.Add("GGM"); var gf = ped.AddChild("GF", ggf, ggm); var gm = ped.AddChild("GM", ggf, ggm); var s = ped.AddChild("S", gf, gm); var d = ped.AddChild("D", gf, gm); var calc = ped.Build(); var result = calc.ForOffspringOf(s, d); Assert.Equal(0.375, result.Coefficient, Precision); // Four common ancestors: GF, GM, GGF, GGM. Assert.Equal(4, result.CommonAncestors.Count); Assert.Equal(0.375, result.CommonAncestors.Sum(a => a.Contribution), Precision); // Deepest known generation above the offspring: S/D (1) → GF/GM (2) → GGF/GGM (3). Assert.Equal(3, result.GenerationsAvailable); } [Fact] public void GenerationsAvailable_CountsDeepestKnownGeneration() { var ped = new PedigreeBuilder(); var gf = ped.Add("GF"); var gm = ped.Add("GM"); var father = ped.AddChild("Father", gf, gm); var mother = ped.Add("Mother"); var calc = ped.Build(); var result = calc.ForOffspringOf(father, mother); // Father side reaches grandparents (2 generations); mother side reaches 1. Assert.Equal(2, result.GenerationsAvailable); } /// Small helper to assemble a pedigree of plain GUIDs for the calculator. private sealed class PedigreeBuilder { private readonly Dictionary _parents = new(); private readonly Dictionary _names = new(); public Guid Add(string name) { var id = Guid.NewGuid(); _parents[id] = (null, null); _names[id] = name; return id; } public Guid AddChild(string name, Guid father, Guid mother) { var id = Add(name); _parents[id] = (father, mother); return id; } public PedigreeCalculator Build() => new(_parents, _names); } } }