diff --git a/GerbilManager.Tests/GerbilManager.Tests.csproj b/GerbilManager.Tests/GerbilManager.Tests.csproj new file mode 100644 index 0000000..d59000b --- /dev/null +++ b/GerbilManager.Tests/GerbilManager.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GerbilManager.Tests/PedigreeCalculatorTests.cs b/GerbilManager.Tests/PedigreeCalculatorTests.cs new file mode 100644 index 0000000..efa12a4 --- /dev/null +++ b/GerbilManager.Tests/PedigreeCalculatorTests.cs @@ -0,0 +1,192 @@ +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); + } + } +} diff --git a/GerbilManager.slnx b/GerbilManager.slnx index 763646b..1b2fb20 100644 --- a/GerbilManager.slnx +++ b/GerbilManager.slnx @@ -1,5 +1,6 @@ +