FEAT-1b: add first test project (xUnit) for Inzuchtkoeffizient
GerbilManager.Tests (net10.0), added to slnx. Reference pedigrees verified against textbook F: parent-offspring 0.25, full sibs 0.25, half sibs 0.125, first cousins 0.0625, unknown 0, plus deep multi-path (0.375) and generationsAvailable checks. 8/8 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
25
GerbilManager.Tests/GerbilManager.Tests.csproj
Normal file
25
GerbilManager.Tests/GerbilManager.Tests.csproj
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GerbilManagerWebAPI\GerbilManagerWebAPI.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
192
GerbilManager.Tests/PedigreeCalculatorTests.cs
Normal file
192
GerbilManager.Tests/PedigreeCalculatorTests.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
using GerbilManagerWebAPI.Genetics;
|
||||||
|
|
||||||
|
namespace GerbilManager.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exact-math checks for the inbreeding coefficient (Inzuchtkoeffizient) against
|
||||||
|
/// the textbook reference values for canonical pedigrees.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Small helper to assemble a pedigree of plain GUIDs for the calculator.</summary>
|
||||||
|
private sealed class PedigreeBuilder
|
||||||
|
{
|
||||||
|
private readonly Dictionary<Guid, (Guid? Father, Guid? Mother)> _parents = new();
|
||||||
|
private readonly Dictionary<Guid, string> _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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="GerbilManager.AppHost/GerbilManager.AppHost.csproj" />
|
<Project Path="GerbilManager.AppHost/GerbilManager.AppHost.csproj" />
|
||||||
<Project Path="GerbilManager.ServiceDefaults/GerbilManager.ServiceDefaults.csproj" />
|
<Project Path="GerbilManager.ServiceDefaults/GerbilManager.ServiceDefaults.csproj" />
|
||||||
|
<Project Path="GerbilManager.Tests/GerbilManager.Tests.csproj" />
|
||||||
<Project Path="GerbilManagerWebAPI/GerbilManagerWebAPI.csproj" />
|
<Project Path="GerbilManagerWebAPI/GerbilManagerWebAPI.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
Reference in New Issue
Block a user