Compare commits
7 Commits
c93c24f37c
...
36db013b76
| Author | SHA1 | Date | |
|---|---|---|---|
| 36db013b76 | |||
| 60d034cde4 | |||
| e2fd32ea12 | |||
| 2f089a902d | |||
| 7aac59f3e5 | |||
| a0b720df73 | |||
| 1e7dc8f91d |
@@ -102,6 +102,23 @@ namespace GerbilManager.Tests
|
||||
Assert.Equal(2, await db.Litters.CountAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_persists_deaf_flag_and_preserves_sls_and_tags()
|
||||
{
|
||||
using var db = NewDb();
|
||||
await new ImportService(db, _dir, _dir).RunAsync(execute: true);
|
||||
|
||||
var a1 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a1");
|
||||
// GEN-3b: deafness is a persisted phenotype flag (NOT a genotype locus).
|
||||
Assert.True(a1.IsDeaf);
|
||||
// Sls (2nd spotting locus) + provenance tags are preserved in RawImportData
|
||||
// (kept out of the 8-locus compact Genotype contract until GEN-3a adopts them).
|
||||
Assert.Contains("Sls", a1.RawImportData!);
|
||||
Assert.Contains("WFNZ", a1.RawImportData!);
|
||||
// and Sls must NOT leak into the compact 8-locus genotype string
|
||||
Assert.DoesNotContain("Sl", a1.Genotype!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComposeGenotype_strips_carets_and_fills_missing_loci()
|
||||
{
|
||||
@@ -139,7 +156,8 @@ namespace GerbilManager.Tests
|
||||
[
|
||||
{"id":"a1","name":"Kind Eins","dob":"01.02.2020","death":"","gender":null,
|
||||
"farbschlag":"Agouti","farbschlagVariants":["Agouti"],
|
||||
"genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"]},"rawGenotype":"aa CC D- ee[f]","unmappedTokens":[]},
|
||||
"genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"],"Sls":["Sl","sl"]},"rawGenotype":"aa CC D- ee[f] WP dea WFNZ","unmappedTokens":[]},
|
||||
"deaf":true,"tags":["WFNZ"],
|
||||
"zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false,
|
||||
"litterRef":{"litterId":"L1","method":"geburtsdatum+eltern","confidence":"hoch"}},
|
||||
{"id":"a2","name":"Streit","dob":"01.01.2019","death":"","gender":null,
|
||||
|
||||
@@ -27,7 +27,8 @@ namespace GerbilManagerWebAPI.Dtos
|
||||
string? ExternalRef,
|
||||
string? OriginBreeder,
|
||||
List<string> CharacterTraits,
|
||||
string? CharacterNote);
|
||||
string? CharacterNote,
|
||||
bool? IsDeaf);
|
||||
|
||||
public record LitterDto(
|
||||
Guid Id,
|
||||
@@ -75,7 +76,8 @@ namespace GerbilManagerWebAPI.Dtos
|
||||
string? ExternalRef,
|
||||
string? OriginBreeder,
|
||||
List<string>? CharacterTraits,
|
||||
string? CharacterNote);
|
||||
string? CharacterNote,
|
||||
bool? IsDeaf);
|
||||
|
||||
public record LitterInput(
|
||||
string Name,
|
||||
|
||||
@@ -97,12 +97,13 @@ namespace GerbilManagerWebAPI.Endpoints
|
||||
g.OriginBreeder = i.OriginBreeder;
|
||||
g.CharacterTraits = i.CharacterTraits ?? new List<string>();
|
||||
g.CharacterNote = i.CharacterNote;
|
||||
g.IsDeaf = i.IsDeaf;
|
||||
}
|
||||
|
||||
internal static GerbilDto ToDto(Gerbil g) => new(
|
||||
g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId,
|
||||
g.EnclosureId, g.ColorVarietyId, g.DateOfBirth, g.DateOfDeath, g.CauseOfDeath,
|
||||
g.GoHomeDate, g.Genotype, g.Notes, g.ImportSource, g.ExternalRef, g.OriginBreeder,
|
||||
g.CharacterTraits, g.CharacterNote);
|
||||
g.CharacterTraits, g.CharacterNote, g.IsDeaf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ namespace GerbilManagerWebAPI.Import
|
||||
public List<string> SourceFiles { get; set; } = new();
|
||||
public bool Conflict { get; set; }
|
||||
public SourceLitterRef? LitterRef { get; set; }
|
||||
|
||||
// GEN-3b normalization: hearing/deaf phenotype flag (null = not stated) and
|
||||
// provenance/breeding tags (WFNZ/RV/GV/DP) — neither is genotype.
|
||||
public bool? Deaf { get; set; }
|
||||
public List<string> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class SourceGenotype
|
||||
|
||||
@@ -174,6 +174,7 @@ namespace GerbilManagerWebAPI.Import
|
||||
LitterId = litterId,
|
||||
ColorVarietyId = colorVarietyId,
|
||||
Genotype = ComposeGenotype(a.Genotype),
|
||||
IsDeaf = a.Deaf,
|
||||
ImportSource = ImportSourceTag,
|
||||
ExternalRef = a.Id,
|
||||
OriginBreeder = string.IsNullOrWhiteSpace(a.Zucht) ? null : a.Zucht.Trim(),
|
||||
@@ -181,6 +182,11 @@ namespace GerbilManagerWebAPI.Import
|
||||
{
|
||||
a.Genotype.RawGenotype,
|
||||
a.Genotype.UnmappedTokens,
|
||||
// GEN-3b: Sls (2nd spotting locus) preserved here until Kevin's GEN-3a
|
||||
// parser adopts it into the compact Genotype contract; tags + deaf too.
|
||||
Sls = a.Genotype.Mapped8locus.TryGetValue("Sls", out var sls) ? sls : null,
|
||||
a.Tags,
|
||||
a.Deaf,
|
||||
a.Zucht,
|
||||
a.SourceFiles,
|
||||
FarbschlagRaw = a.Farbschlag,
|
||||
|
||||
1381
GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.Designer.cs
generated
Normal file
1381
GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GerbilManagerWebAPI.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGerbilDeafFlag : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeaf",
|
||||
table: "Gerbils",
|
||||
type: "boolean",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeaf",
|
||||
table: "Gerbils");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -781,6 +781,9 @@ namespace GerbilManagerWebAPI.Migrations
|
||||
b.Property<string>("ImportSource")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool?>("IsDeaf")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid?>("LitterId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
|
||||
@@ -67,6 +67,12 @@ namespace GerbilManagerWebAPI.Models
|
||||
|
||||
/// <summary>FEAT-14: free-text character note; feeds the AI Verkaufstext.</summary>
|
||||
public string? CharacterNote { get; set; }
|
||||
|
||||
/// <summary>GEN-3b: hearing/deaf phenotype flag (NOT a genotype locus — it's the
|
||||
/// downstream effect of high white load / Sp×Sls). null = not stated, true = deaf
|
||||
/// (dea/taub), false = hearing (Dea/hörend). Set by the FEAT-8 import from the
|
||||
/// after-spsp deafness annotation; see hive/agents/god/GENETIK-notation.md.</summary>
|
||||
public bool? IsDeaf { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Shared normalisation for the separator-insensitive name search.</summary>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
toJSON,
|
||||
fromJSON,
|
||||
wildType,
|
||||
extractGenotypeFlags,
|
||||
} from '../genotype'
|
||||
import { combineLocus } from '../punnett'
|
||||
import { LOCI, type LocusKey } from '../loci'
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
} from '../catalog'
|
||||
|
||||
/** The first 18 entries are the frozen contract names (must round-trip exactly). */
|
||||
const FROZEN_COUNT = 18
|
||||
const FROZEN_COUNT = 17
|
||||
import { breed } from '../breed'
|
||||
import { GeneticsWarningCode } from '../warnings'
|
||||
|
||||
@@ -64,6 +65,7 @@ describe('Genotype serialization', () => {
|
||||
P: ['p', 'P'],
|
||||
Sp: ['sp', 'sp'],
|
||||
Re: ['re', 're'],
|
||||
Sls: ['sl', 'sl'],
|
||||
})
|
||||
expect(g.A).toEqual(['A', 'a'])
|
||||
expect(g.C).toEqual(['C', 'ch'])
|
||||
@@ -240,6 +242,7 @@ describe('Partially-unknown parents (wildcards)', () => {
|
||||
P: ['P', 'P'],
|
||||
Sp: ['sp', 'sp'],
|
||||
Re: ['re', 're'],
|
||||
Sls: ['sl', 'sl'],
|
||||
})
|
||||
const mother = fromDisplayString('aa CC DD EE GG PP spsp rere')
|
||||
const result = breed(father, mother)
|
||||
@@ -263,3 +266,87 @@ describe('Probabilities are exact fractions summing to 1', () => {
|
||||
expect(toNumber(sum)).toBeCloseTo(1, 10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-3a: Uw=G alias', () => {
|
||||
it('Uwuw parses as Gg, UwUw as GG', () => {
|
||||
expect(fromDisplayString('AA CC DD EE Uwuw PP spsp rere').G).toEqual(['G', 'g'])
|
||||
expect(fromDisplayString('AA CC DD EE UwUw PP spsp rere').G).toEqual(['G', 'G'])
|
||||
expect(fromDisplayString('AA CC DD EE uwuw PP spsp rere').G).toEqual(['g', 'g'])
|
||||
})
|
||||
|
||||
it('always RENDERS G, never Uw (breeder preference)', () => {
|
||||
// Uw/uw is an input/import alias only; output must echo G/g.
|
||||
expect(toDisplayString(fromDisplayString('AA CC DD EE Uwuw PP spsp rere'))).toBe(
|
||||
'AA CC DD EE Gg PP spsp rere',
|
||||
)
|
||||
expect(toDisplayString(fromDisplayString('AA CC DD EE uwuw PP spsp rere'))).not.toContain('uw')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-3a: second spotting locus Sls (WP)', () => {
|
||||
it('S(l)s(l) and WP both parse to the Sls heterozygote Slsl', () => {
|
||||
expect(fromDisplayString('AA CC DD EE GG PP spsp rere S(l)s(l)').Sls).toEqual(['Sl', 'sl'])
|
||||
expect(fromDisplayString('AA CC DD EE GG PP spsp rere WP').Sls).toEqual(['Sl', 'sl'])
|
||||
})
|
||||
|
||||
it('toDisplayString omits wild-type Sls but shows Slsl', () => {
|
||||
expect(toDisplayString(wildType())).toBe('AA CC DD EE GG PP spsp rere')
|
||||
expect(toDisplayString(fromDisplayString('AA CC DD EE GG PP spsp rere WP'))).toBe(
|
||||
'AA CC DD EE GG PP spsp rere Slsl',
|
||||
)
|
||||
})
|
||||
|
||||
it('WP × WP: S(l)S(l) is lethal — removed, renormalized, SLS_LETHAL warning', () => {
|
||||
const parent = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl')
|
||||
const result = breed(parent, parent)
|
||||
expect(result.offspring.every((o) => !o.genotype.includes('SlSl'))).toBe(true)
|
||||
const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
|
||||
expect(sum).toBeCloseTo(1, 10)
|
||||
// survivors 2/3 Slsl : 1/3 slsl
|
||||
const wp = result.offspring.find((o) => o.genotype.includes('Slsl'))!
|
||||
expect(wp.probability.text).toBe('2/3')
|
||||
const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.SlsLethal)
|
||||
expect(warn?.detail?.youngLostFraction).toBe('1/4')
|
||||
})
|
||||
|
||||
it('Sp × Sls → Superschecke deafness warning', () => {
|
||||
const father = fromDisplayString('AA CC DD EE GG PP Spsp rere')
|
||||
const mother = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl')
|
||||
const result = breed(father, mother)
|
||||
expect(
|
||||
result.warnings.some((w) => w.code === GeneticsWarningCode.SuperscheckeDeaf),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-3a: flag/metadata tokens tolerated', () => {
|
||||
it('dea/taub/Dea/DP/WFNZ/RV/GV do not break parsing (stripped)', () => {
|
||||
const g = fromDisplayString('AA CC DD EE GG PP spsp rere dea WFNZ DP RV GV')
|
||||
expect(toDisplayString(g)).toBe('AA CC DD EE GG PP spsp rere')
|
||||
})
|
||||
|
||||
it('extractGenotypeFlags reads deafness + tags', () => {
|
||||
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere dea WFNZ').deaf).toBe(true)
|
||||
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere Dea').deaf).toBe(false)
|
||||
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere WFNZ RV').tags).toEqual(['WFNZ', 'RV'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-3a: Schimmel catalog fix (breeder C5)', () => {
|
||||
it('efef base -> Orangeschimmel (not Schwarzschimmel)', () => {
|
||||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG PP spsp rere'))).toBe(
|
||||
'Orangeschimmel',
|
||||
)
|
||||
})
|
||||
it('efef pp -> Rotaugenschimmel, efef gg -> Silberschimmel', () => {
|
||||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG pp spsp rere'))).toBe(
|
||||
'Rotaugenschimmel',
|
||||
)
|
||||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef gg PP spsp rere'))).toBe(
|
||||
'Silberschimmel',
|
||||
)
|
||||
})
|
||||
it('no Schwarzschimmel anywhere in the catalog', () => {
|
||||
expect(BASE_COLORS.some((e) => e.name === 'Schwarzschimmel')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,12 +41,14 @@ export interface FarbschlagEntry {
|
||||
* portal collision group.
|
||||
*/
|
||||
export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
// ── Frozen 18 (names are DB-key contract; do not rename) ──
|
||||
// ── Frozen names (DB-key contract; do not rename) ──
|
||||
// GEN-3a: 'Schwarzschimmel' REMOVED (breeder C5: no such variety; efef base is
|
||||
// Orangeschimmel — see the GEN-2 block below). This was an authorized exception
|
||||
// to the frozen-name rule; the ColorVariety seed drops it too.
|
||||
{ name: 'Pink Eyed White (PEW)', english: 'Pink Eyed White', tokens: { C: 'ch', P: 'p' }, image: 'rotaugen-weiss-pew-d-sep-e-sep.jpg' },
|
||||
{ name: 'Hermelin', english: 'Dark Tailed White', tokens: { A: 'a', C: 'ch', D: 'D', P: 'P' }, image: 'hermelin.jpeg' },
|
||||
{ name: 'Himalaya', english: 'Himalayan', tokens: { A: 'A', C: 'ch', D: 'D', P: 'P' }, image: 'himalaya.jpg' },
|
||||
{ name: 'Zobel', english: 'Sable', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel.jpeg' },
|
||||
{ name: 'Schwarzschimmel', english: 'Black Roan', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
||||
{ name: 'Rotaugenschimmel', english: 'Red-Eyed Roan', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'p' }, image: 'rotaugen-schimmel.jpg' },
|
||||
{ name: 'Agouti', english: 'Golden Agouti', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-mit-erklaerung-der-genloci.JPG' },
|
||||
{ name: 'Schwarz', english: 'Black', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'schwarz.jpg' },
|
||||
@@ -72,7 +74,8 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ name: 'Siam (Marder-Hell)', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'siam-marder-hell.JPG' },
|
||||
{ name: 'Polarfuchs', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs.jpg' },
|
||||
{ name: 'Saphir', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'saphir.jpg' },
|
||||
{ name: 'Schimmel (Orangeschimmel)', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'schimmel-orangeschimmel.jpg' },
|
||||
// GEN-3a: efef base (otherwise wild C/D/G/P) = Orangeschimmel (breeder C5).
|
||||
{ name: 'Orangeschimmel', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'schimmel-orangeschimmel.jpg' },
|
||||
{ name: 'Topas', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'topas.jpg' },
|
||||
{ name: 'Platin-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin-hell.jpg' },
|
||||
{ name: 'Agouti dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' }, image: 'agouti-dd.jpg' },
|
||||
@@ -81,8 +84,10 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ name: 'Anthrazit dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'anthrazit-dd.jpg' },
|
||||
{ name: 'Agouti CP-Hell', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-cp-hell.JPG' },
|
||||
{ name: 'Blaufuchs CP', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'blaufuchs-cp.jpg' },
|
||||
{ name: 'Polarfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'polarfuchsschimmel.jpg' },
|
||||
// GEN-3a: efef gg base = Silberschimmel (breeder C5) — listed before the
|
||||
// A-specific Polarfuchsschimmel so the canonical efef-gg reverse-matches here.
|
||||
{ name: 'Silberschimmel', tokens: { C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'silberschimmel.jpg' },
|
||||
{ name: 'Polarfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'polarfuchsschimmel.jpg' },
|
||||
{ name: 'Algierfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'algierfuchsschimmel.jpg' },
|
||||
{ name: 'Polarfuchs-Hell CP', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs-hell-cp.jpg' },
|
||||
{ name: 'Kohlfuchsschimmel', tokens: { A: 'a', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'kohlfuchsschimmel.jpg' },
|
||||
@@ -117,6 +122,9 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ name: 'Zobel-Hell dd', tokens: { A: 'a', C: 'cchm', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'zobel-hell-dd.jpg' },
|
||||
{ name: 'Kohlfuchsschimmel CP', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'kohlfuchsschimmel-cp.JPG' },
|
||||
{ name: 'Blaufuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' },
|
||||
// GEN-3a: c[chm]c[chm] efef base = CP-Orangeschimmel (breeder C5). A unspecified
|
||||
// (listed after the A-specific cchm-Schimmel entries so they win for those).
|
||||
{ name: 'CP-Orangeschimmel', tokens: { C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
||||
]
|
||||
|
||||
export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag'
|
||||
|
||||
@@ -1,440 +1,456 @@
|
||||
[
|
||||
{
|
||||
"name": "Pink Eyed White (PEW)",
|
||||
"canonicalGenotype": "AA chch DD EE GG pp spsp rere",
|
||||
"sortOrder": 0,
|
||||
"image": "rotaugen-weiss-pew-d-sep-e-sep.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Hermelin",
|
||||
"canonicalGenotype": "aa chch DD EE GG PP spsp rere",
|
||||
"sortOrder": 1,
|
||||
"image": "hermelin.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Himalaya",
|
||||
"canonicalGenotype": "AA chch DD EE GG PP spsp rere",
|
||||
"sortOrder": 2,
|
||||
"image": "himalaya.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere",
|
||||
"sortOrder": 3,
|
||||
"image": "zobel.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Schwarzschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 4,
|
||||
"image": null
|
||||
},
|
||||
{
|
||||
"name": "Rotaugenschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef GG pp spsp rere",
|
||||
"sortOrder": 5,
|
||||
"image": "rotaugen-schimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti",
|
||||
"canonicalGenotype": "AA CC DD EE GG PP spsp rere",
|
||||
"sortOrder": 6,
|
||||
"image": "agouti-mit-erklaerung-der-genloci.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Schwarz",
|
||||
"canonicalGenotype": "aa CC DD EE GG PP spsp rere",
|
||||
"sortOrder": 7,
|
||||
"image": "schwarz.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti",
|
||||
"canonicalGenotype": "AA CC DD EE gg PP spsp rere",
|
||||
"sortOrder": 8,
|
||||
"image": "silberagouti.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Anthrazit",
|
||||
"canonicalGenotype": "aa CC DD EE gg PP spsp rere",
|
||||
"sortOrder": 9,
|
||||
"image": "anthrazit.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs",
|
||||
"canonicalGenotype": "AA CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 10,
|
||||
"image": "algierfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blau",
|
||||
"canonicalGenotype": "aa CC dd EE GG PP spsp rere",
|
||||
"sortOrder": 11,
|
||||
"image": "blau-schwarz-dd.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Gold",
|
||||
"canonicalGenotype": "AA CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 12,
|
||||
"image": "gold.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Platin",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 13,
|
||||
"image": "platin.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Goldfuchs",
|
||||
"canonicalGenotype": "AA CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 14,
|
||||
"image": "goldfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Rotfuchs",
|
||||
"canonicalGenotype": "aa CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 15,
|
||||
"image": "rotfuchs.JPG"
|
||||
},
|
||||
{
|
||||
"name": "dd Gold",
|
||||
"canonicalGenotype": "AA CC dd EE GG pp spsp rere",
|
||||
"sortOrder": 16,
|
||||
"image": "gold-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "dd Platin",
|
||||
"canonicalGenotype": "aa CC dd EE GG pp spsp rere",
|
||||
"sortOrder": 17,
|
||||
"image": "platin-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Altweiss (REW)",
|
||||
"canonicalGenotype": "aa CC DD EE gg pp spsp rere",
|
||||
"sortOrder": 18,
|
||||
"image": "altweiss-rew.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Apricot (Blassfuchs)",
|
||||
"canonicalGenotype": "AA CC DD ee gg pp spsp rere",
|
||||
"sortOrder": 19,
|
||||
"image": "apricot-blassfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs",
|
||||
"canonicalGenotype": "aa CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 20,
|
||||
"image": "blaufuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "C-Separator",
|
||||
"canonicalGenotype": "aa CC DD ee gg pp spsp rere",
|
||||
"sortOrder": 21,
|
||||
"image": "c-separator.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Elfenbein",
|
||||
"canonicalGenotype": "AA CC DD EE gg pp spsp rere",
|
||||
"sortOrder": 22,
|
||||
"image": "elfenbein.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs",
|
||||
"canonicalGenotype": "aa CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 23,
|
||||
"image": "kohlfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Marder",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 24,
|
||||
"image": "marder.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Siam (Marder-Hell)",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 25,
|
||||
"image": "siam-marder-hell.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs",
|
||||
"canonicalGenotype": "AA CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 26,
|
||||
"image": "polarfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Saphir",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 27,
|
||||
"image": "saphir.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Schimmel (Orangeschimmel)",
|
||||
"canonicalGenotype": "AA CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 28,
|
||||
"image": "schimmel-orangeschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Topas",
|
||||
"canonicalGenotype": "AA CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 29,
|
||||
"image": "topas.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Platin-Hell",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 30,
|
||||
"image": "platin-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti dd",
|
||||
"canonicalGenotype": "AA CC dd EE GG PP spsp rere",
|
||||
"sortOrder": 31,
|
||||
"image": "agouti-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti dd",
|
||||
"canonicalGenotype": "AA CC dd EE gg PP spsp rere",
|
||||
"sortOrder": 32,
|
||||
"image": "silberagouti-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs dd",
|
||||
"canonicalGenotype": "aa CC dd ee GG PP spsp rere",
|
||||
"sortOrder": 33,
|
||||
"image": "kohlfuchs-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Anthrazit dd",
|
||||
"canonicalGenotype": "aa CC dd EE gg PP spsp rere",
|
||||
"sortOrder": 34,
|
||||
"image": "anthrazit-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti CP-Hell",
|
||||
"canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 35,
|
||||
"image": "agouti-cp-hell.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD ee gg PP spsp rere",
|
||||
"sortOrder": 36,
|
||||
"image": "blaufuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchsschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef gg PP spsp rere",
|
||||
"sortOrder": 37,
|
||||
"image": "polarfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef gg PP spsp rere",
|
||||
"sortOrder": 38,
|
||||
"image": "silberschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchsschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 39,
|
||||
"image": "algierfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs-Hell CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
||||
"sortOrder": 40,
|
||||
"image": "polarfuchs-hell-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchsschimmel",
|
||||
"canonicalGenotype": "aa CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 41,
|
||||
"image": "kohlfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchsschimmel",
|
||||
"canonicalGenotype": "aa CC DD efef gg PP spsp rere",
|
||||
"sortOrder": 42,
|
||||
"image": "blaufuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs, hell",
|
||||
"canonicalGenotype": "aa CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 43,
|
||||
"image": "kohlfuchs-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Goldfuchs, hell",
|
||||
"canonicalGenotype": "AA CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 44,
|
||||
"image": "goldfuchs-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Goldfuchsschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef GG pp spsp rere",
|
||||
"sortOrder": 45,
|
||||
"image": "goldfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Gold-Hell",
|
||||
"canonicalGenotype": "AA CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 46,
|
||||
"image": "gold-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Siam (Marder-Hell) dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 47,
|
||||
"image": "siam-marder-hell-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Marder dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 48,
|
||||
"image": "marder-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel-Hell",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere",
|
||||
"sortOrder": 49,
|
||||
"image": "zobel-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 50,
|
||||
"image": "silberagouti-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti-Hell dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 51,
|
||||
"image": "silberagouti-hell-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 52,
|
||||
"image": "agouti-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti-Hell dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 53,
|
||||
"image": "agouti-hell-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs, hell",
|
||||
"canonicalGenotype": "aa CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 54,
|
||||
"image": "blaufuchs-hell.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Rotfuchsschimmel",
|
||||
"canonicalGenotype": "aa CC DD efef GG pp spsp rere",
|
||||
"sortOrder": 55,
|
||||
"image": "rotfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs, hell",
|
||||
"canonicalGenotype": "AA CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 56,
|
||||
"image": "polarfuchs-hell.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchsschimmel, hell",
|
||||
"canonicalGenotype": "aa CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 57,
|
||||
"image": "kohlfuchsschimmel-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Rotfuchs, hell",
|
||||
"canonicalGenotype": "aa CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 58,
|
||||
"image": "rotfuchs-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 59,
|
||||
"image": "zobel-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs-Hell",
|
||||
"canonicalGenotype": "aa CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 60,
|
||||
"image": "kohlfuchs-hell-2.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 61,
|
||||
"image": "kohlfuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 62,
|
||||
"image": "algierfuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere",
|
||||
"sortOrder": 63,
|
||||
"image": "silberagouti-cp.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Agouti CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 64,
|
||||
"image": "agouti-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs-Hell CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 65,
|
||||
"image": "algierfuchs-hell-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs,hell CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 66,
|
||||
"image": "kohlfuchs-hell-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
||||
"sortOrder": 67,
|
||||
"image": "polarfuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs, hell",
|
||||
"canonicalGenotype": "AA CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 68,
|
||||
"image": "algierfuchs-hell.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Topas dd",
|
||||
"canonicalGenotype": "AA CC dd EE GG pp spsp rere",
|
||||
"sortOrder": 69,
|
||||
"image": "topas-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel-Hell dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 70,
|
||||
"image": "zobel-hell-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchsschimmel CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD efef GG PP spsp rere",
|
||||
"sortOrder": 71,
|
||||
"image": "kohlfuchsschimmel-cp.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs dd",
|
||||
"canonicalGenotype": "aa CC dd ee gg PP spsp rere",
|
||||
"sortOrder": 72,
|
||||
"image": "blaufuchs-dd.jpg"
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"name": "Pink Eyed White (PEW)",
|
||||
"english": "Pink Eyed White",
|
||||
"canonicalGenotype": "AA chch DD EE GG pp spsp rere",
|
||||
"sortOrder": 0,
|
||||
"image": "rotaugen-weiss-pew-d-sep-e-sep.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Hermelin",
|
||||
"english": "Dark Tailed White",
|
||||
"canonicalGenotype": "aa chch DD EE GG PP spsp rere",
|
||||
"sortOrder": 1,
|
||||
"image": "hermelin.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Himalaya",
|
||||
"english": "Himalayan",
|
||||
"canonicalGenotype": "AA chch DD EE GG PP spsp rere",
|
||||
"sortOrder": 2,
|
||||
"image": "himalaya.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel",
|
||||
"english": "Sable",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere",
|
||||
"sortOrder": 3,
|
||||
"image": "zobel.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Rotaugenschimmel",
|
||||
"english": "Red-Eyed Roan",
|
||||
"canonicalGenotype": "AA CC DD efef GG pp spsp rere",
|
||||
"sortOrder": 4,
|
||||
"image": "rotaugen-schimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti",
|
||||
"english": "Golden Agouti",
|
||||
"canonicalGenotype": "AA CC DD EE GG PP spsp rere",
|
||||
"sortOrder": 5,
|
||||
"image": "agouti-mit-erklaerung-der-genloci.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Schwarz",
|
||||
"english": "Black",
|
||||
"canonicalGenotype": "aa CC DD EE GG PP spsp rere",
|
||||
"sortOrder": 6,
|
||||
"image": "schwarz.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti",
|
||||
"english": "Grey Agouti",
|
||||
"canonicalGenotype": "AA CC DD EE gg PP spsp rere",
|
||||
"sortOrder": 7,
|
||||
"image": "silberagouti.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Anthrazit",
|
||||
"english": "Slate",
|
||||
"canonicalGenotype": "aa CC DD EE gg PP spsp rere",
|
||||
"sortOrder": 8,
|
||||
"image": "anthrazit.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs",
|
||||
"english": "Dark-Eyed Honey",
|
||||
"canonicalGenotype": "AA CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 9,
|
||||
"image": "algierfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blau",
|
||||
"english": "Blue",
|
||||
"canonicalGenotype": "aa CC dd EE GG PP spsp rere",
|
||||
"sortOrder": 10,
|
||||
"image": "blau-schwarz-dd.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Gold",
|
||||
"english": "Argente Golden",
|
||||
"canonicalGenotype": "AA CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 11,
|
||||
"image": "gold.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Platin",
|
||||
"english": "Lilac",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 12,
|
||||
"image": "platin.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Goldfuchs",
|
||||
"english": "Yellow Fox",
|
||||
"canonicalGenotype": "AA CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 13,
|
||||
"image": "goldfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Rotfuchs",
|
||||
"english": "Argente Nutmeg",
|
||||
"canonicalGenotype": "aa CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 14,
|
||||
"image": "rotfuchs.JPG"
|
||||
},
|
||||
{
|
||||
"name": "dd Gold",
|
||||
"english": "dd Argente Golden",
|
||||
"canonicalGenotype": "AA CC dd EE GG pp spsp rere",
|
||||
"sortOrder": 15,
|
||||
"image": "gold-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "dd Platin",
|
||||
"english": "dd Lilac",
|
||||
"canonicalGenotype": "aa CC dd EE GG pp spsp rere",
|
||||
"sortOrder": 16,
|
||||
"image": "platin-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Altweiss (REW)",
|
||||
"canonicalGenotype": "aa CC DD EE gg pp spsp rere",
|
||||
"sortOrder": 17,
|
||||
"image": "altweiss-rew.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Apricot (Blassfuchs)",
|
||||
"canonicalGenotype": "AA CC DD ee gg pp spsp rere",
|
||||
"sortOrder": 18,
|
||||
"image": "apricot-blassfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs",
|
||||
"canonicalGenotype": "aa CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 19,
|
||||
"image": "blaufuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "C-Separator",
|
||||
"canonicalGenotype": "aa CC DD ee gg pp spsp rere",
|
||||
"sortOrder": 20,
|
||||
"image": "c-separator.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Elfenbein",
|
||||
"canonicalGenotype": "AA CC DD EE gg pp spsp rere",
|
||||
"sortOrder": 21,
|
||||
"image": "elfenbein.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs",
|
||||
"canonicalGenotype": "aa CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 22,
|
||||
"image": "kohlfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Marder",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 23,
|
||||
"image": "marder.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Siam (Marder-Hell)",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 24,
|
||||
"image": "siam-marder-hell.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs",
|
||||
"canonicalGenotype": "AA CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 25,
|
||||
"image": "polarfuchs.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Saphir",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 26,
|
||||
"image": "saphir.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Orangeschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 27,
|
||||
"image": "schimmel-orangeschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Topas",
|
||||
"canonicalGenotype": "AA CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 28,
|
||||
"image": "topas.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Platin-Hell",
|
||||
"canonicalGenotype": "aa CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 29,
|
||||
"image": "platin-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti dd",
|
||||
"canonicalGenotype": "AA CC dd EE GG PP spsp rere",
|
||||
"sortOrder": 30,
|
||||
"image": "agouti-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti dd",
|
||||
"canonicalGenotype": "AA CC dd EE gg PP spsp rere",
|
||||
"sortOrder": 31,
|
||||
"image": "silberagouti-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs dd",
|
||||
"canonicalGenotype": "aa CC dd ee GG PP spsp rere",
|
||||
"sortOrder": 32,
|
||||
"image": "kohlfuchs-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Anthrazit dd",
|
||||
"canonicalGenotype": "aa CC dd EE gg PP spsp rere",
|
||||
"sortOrder": 33,
|
||||
"image": "anthrazit-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti CP-Hell",
|
||||
"canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 34,
|
||||
"image": "agouti-cp-hell.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD ee gg PP spsp rere",
|
||||
"sortOrder": 35,
|
||||
"image": "blaufuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef gg PP spsp rere",
|
||||
"sortOrder": 36,
|
||||
"image": "silberschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchsschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef gg PP spsp rere",
|
||||
"sortOrder": 37,
|
||||
"image": "polarfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchsschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 38,
|
||||
"image": "algierfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs-Hell CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
||||
"sortOrder": 39,
|
||||
"image": "polarfuchs-hell-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchsschimmel",
|
||||
"canonicalGenotype": "aa CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 40,
|
||||
"image": "kohlfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchsschimmel",
|
||||
"canonicalGenotype": "aa CC DD efef gg PP spsp rere",
|
||||
"sortOrder": 41,
|
||||
"image": "blaufuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs, hell",
|
||||
"canonicalGenotype": "aa CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 42,
|
||||
"image": "kohlfuchs-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Goldfuchs, hell",
|
||||
"canonicalGenotype": "AA CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 43,
|
||||
"image": "goldfuchs-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Goldfuchsschimmel",
|
||||
"canonicalGenotype": "AA CC DD efef GG pp spsp rere",
|
||||
"sortOrder": 44,
|
||||
"image": "goldfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Gold-Hell",
|
||||
"canonicalGenotype": "AA CC DD EE GG pp spsp rere",
|
||||
"sortOrder": 45,
|
||||
"image": "gold-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Siam (Marder-Hell) dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 46,
|
||||
"image": "siam-marder-hell-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Marder dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 47,
|
||||
"image": "marder-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel-Hell",
|
||||
"canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere",
|
||||
"sortOrder": 48,
|
||||
"image": "zobel-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 49,
|
||||
"image": "silberagouti-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti-Hell dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 50,
|
||||
"image": "silberagouti-hell-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 51,
|
||||
"image": "agouti-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Agouti-Hell dd CP",
|
||||
"canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere",
|
||||
"sortOrder": 52,
|
||||
"image": "agouti-hell-dd-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs, hell",
|
||||
"canonicalGenotype": "aa CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 53,
|
||||
"image": "blaufuchs-hell.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Rotfuchsschimmel",
|
||||
"canonicalGenotype": "aa CC DD efef GG pp spsp rere",
|
||||
"sortOrder": 54,
|
||||
"image": "rotfuchsschimmel.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs, hell",
|
||||
"canonicalGenotype": "AA CC DD ee gg PP spsp rere",
|
||||
"sortOrder": 55,
|
||||
"image": "polarfuchs-hell.jpeg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchsschimmel, hell",
|
||||
"canonicalGenotype": "aa CC DD efef GG PP spsp rere",
|
||||
"sortOrder": 56,
|
||||
"image": "kohlfuchsschimmel-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Rotfuchs, hell",
|
||||
"canonicalGenotype": "aa CC DD ee GG pp spsp rere",
|
||||
"sortOrder": 57,
|
||||
"image": "rotfuchs-hell.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 58,
|
||||
"image": "zobel-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs-Hell",
|
||||
"canonicalGenotype": "aa CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 59,
|
||||
"image": "kohlfuchs-hell-2.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 60,
|
||||
"image": "kohlfuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 61,
|
||||
"image": "algierfuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Silberagouti CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere",
|
||||
"sortOrder": 62,
|
||||
"image": "silberagouti-cp.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Agouti CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere",
|
||||
"sortOrder": 63,
|
||||
"image": "agouti-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs-Hell CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 64,
|
||||
"image": "algierfuchs-hell-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchs,hell CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere",
|
||||
"sortOrder": 65,
|
||||
"image": "kohlfuchs-hell-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Polarfuchs CP",
|
||||
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
||||
"sortOrder": 66,
|
||||
"image": "polarfuchs-cp.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Algierfuchs, hell",
|
||||
"canonicalGenotype": "AA CC DD ee GG PP spsp rere",
|
||||
"sortOrder": 67,
|
||||
"image": "algierfuchs-hell.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Topas dd",
|
||||
"canonicalGenotype": "AA CC dd EE GG pp spsp rere",
|
||||
"sortOrder": 68,
|
||||
"image": "topas-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Zobel-Hell dd",
|
||||
"canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere",
|
||||
"sortOrder": 69,
|
||||
"image": "zobel-hell-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Kohlfuchsschimmel CP",
|
||||
"canonicalGenotype": "aa cchmcchm DD efef GG PP spsp rere",
|
||||
"sortOrder": 70,
|
||||
"image": "kohlfuchsschimmel-cp.JPG"
|
||||
},
|
||||
{
|
||||
"name": "Blaufuchs dd",
|
||||
"canonicalGenotype": "aa CC dd ee gg pp spsp rere",
|
||||
"sortOrder": 71,
|
||||
"image": "blaufuchs-dd.jpg"
|
||||
},
|
||||
{
|
||||
"name": "CP-Orangeschimmel",
|
||||
"canonicalGenotype": "AA cchmcchm DD efef GG PP spsp rere",
|
||||
"sortOrder": 72
|
||||
}
|
||||
]
|
||||
|
||||
@@ -64,18 +64,28 @@ export function makeGenotype(input: Record<LocusKey, AllelePair>): Genotype {
|
||||
export function wildType(): Genotype {
|
||||
const out = {} as Record<LocusKey, AllelePair>
|
||||
for (const locus of LOCUS_ORDER) {
|
||||
// Wild-type is homozygous for the most dominant allele, EXCEPT the
|
||||
// marker loci Sp/Re whose wild form is the recessive (unmarked) allele.
|
||||
// Wild-type is homozygous for the most dominant allele, EXCEPT the marker
|
||||
// loci Sp/Re/Sls whose wild form is the recessive (unmarked) allele.
|
||||
const alleles = LOCI[locus].alleles
|
||||
const a = locus === 'Sp' || locus === 'Re' ? alleles[alleles.length - 1] : alleles[0]
|
||||
const marker = locus === 'Sp' || locus === 'Re' || locus === 'Sls'
|
||||
const a = marker ? alleles[alleles.length - 1] : alleles[0]
|
||||
out[locus] = [a, a]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Compact display string, e.g. "Aa CC Dd EE GG Pp spsp rere". */
|
||||
/**
|
||||
* Compact display string, e.g. "Aa CC Dd EE GG Pp spsp rere".
|
||||
* The Sls locus is OMITTED when wild-type (sl/sl) so legacy 8-locus strings and
|
||||
* the colour catalog stay byte-identical; it only appears for WP/Sls carriers
|
||||
* (e.g. "… spsp rere Slsl"). Round-trips: a missing Sls re-parses to sl/sl.
|
||||
*/
|
||||
export function toDisplayString(g: Genotype): string {
|
||||
return LOCUS_ORDER.map((locus) => g[locus][0] + g[locus][1]).join(' ')
|
||||
return LOCUS_ORDER.filter(
|
||||
(locus) => locus !== 'Sls' || !(g.Sls[0] === 'sl' && g.Sls[1] === 'sl'),
|
||||
)
|
||||
.map((locus) => g[locus][0] + g[locus][1])
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/** Stable JSON-storable object (already the in-memory shape; returned as a copy). */
|
||||
@@ -114,14 +124,72 @@ function splitToken(token: string): [string, string] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a display string ("Aa CC Dd EE GG Pp Spsp rere") back into a Genotype.
|
||||
* Tokens may be given in any order; each token must belong to a distinct locus.
|
||||
* GEN-3a: tokens that are NOT genotype loci — health/provenance metadata that may
|
||||
* appear in a herd-book genotype string. Stripped on parse (see extractGenotypeFlags).
|
||||
* - dea/Dea/taub = deafness flag (after spsp); DP/DarkPatch = non-Mendelian patch flag
|
||||
* - WFNZ/RV/GV = provenance/breeding-method annotations
|
||||
*/
|
||||
const FLAG_TOKENS = new Set(['DP', 'DarkPatch', 'dea', 'Dea', 'taub', 'WFNZ', 'RV', 'GV'])
|
||||
|
||||
/**
|
||||
* Normalize one whitespace-token to canonical allele symbols, or null if it is a
|
||||
* non-genotype flag/metadata token (to be stripped):
|
||||
* - Uw/uw -> G/g (international Underwhite == German Grey locus)
|
||||
* - S(l)/s(l) -> Sl/sl (second spotting locus notation)
|
||||
* - WP -> Slsl (WP is the visible S(l)s(l) heterozygote)
|
||||
*/
|
||||
function normalizeToken(tok: string): string | null {
|
||||
if (FLAG_TOKENS.has(tok)) return null
|
||||
let t = tok
|
||||
if (t === 'WP') t = 'Slsl'
|
||||
t = t.replace(/S\(l\)/g, 'Sl').replace(/s\(l\)/g, 'sl')
|
||||
t = t.replace(/Uw/g, 'G').replace(/uw/g, 'g')
|
||||
return t
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical normalized genotype string (flags/metadata removed, Uw/S(l)/WP
|
||||
* resolved). Exported so the import pipeline (GEN-3b) can mirror this exactly.
|
||||
*/
|
||||
export function normalizeGenotypeString(input: string): string {
|
||||
return input
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map(normalizeToken)
|
||||
.filter((t): t is string => t !== null)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract non-Punkett flags from a raw genotype string: deafness (dea/taub =
|
||||
* deaf, Dea = hearing) and provenance/pattern tags (WFNZ/RV/GV/DP).
|
||||
*/
|
||||
export function extractGenotypeFlags(input: string): { deaf?: boolean; tags: string[] } {
|
||||
const tokens = input.trim().split(/\s+/).filter(Boolean)
|
||||
let deaf: boolean | undefined
|
||||
const tags: string[] = []
|
||||
for (const tok of tokens) {
|
||||
if (tok === 'dea' || tok === 'taub') deaf = true
|
||||
else if (tok === 'Dea') deaf = false
|
||||
else if (tok === 'DP' || tok === 'DarkPatch' || tok === 'WFNZ' || tok === 'RV' || tok === 'GV')
|
||||
tags.push(tok)
|
||||
}
|
||||
return { deaf, tags }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a display string ("Aa CC Dd EE GG Pp Spsp rere [Slsl]") back into a
|
||||
* Genotype. Tokens may be in any order; each must belong to a distinct locus.
|
||||
* Uw/S(l)/WP are normalized and flag/metadata tokens (dea, WFNZ, …) are stripped.
|
||||
* Missing loci default to wild-type.
|
||||
*/
|
||||
export function fromDisplayString(input: string): Genotype {
|
||||
const tokens = input.trim().split(/\s+/).filter(Boolean)
|
||||
const acc = {} as Record<LocusKey, AllelePair>
|
||||
for (const token of tokens) {
|
||||
for (const raw of tokens) {
|
||||
const token = normalizeToken(raw)
|
||||
if (token === null) continue // flag/metadata token — not a locus
|
||||
const [a, b] = splitToken(token)
|
||||
const refAllele = a === WILDCARD ? b : a
|
||||
if (refAllele === WILDCARD) {
|
||||
|
||||
@@ -25,6 +25,7 @@ interface LethalRule {
|
||||
|
||||
const LETHAL_RULES: readonly LethalRule[] = [
|
||||
{ locus: 'Sp', allele: 'Sp', kind: 'lethal', warning: GeneticsWarningCode.ScheckeLethal },
|
||||
{ locus: 'Sls', allele: 'Sl', kind: 'lethal', warning: GeneticsWarningCode.SlsLethal },
|
||||
{ locus: 'Re', allele: 'Re', kind: 'semi', warning: GeneticsWarningCode.RexSemiLethal },
|
||||
]
|
||||
|
||||
@@ -67,14 +68,23 @@ export function applyLethality(dist: DistEntry<Genotype>[]): LethalityResult {
|
||||
? survivors
|
||||
: survivors.map((e) => ({ value: e.value, probability: divide(e.probability, survivingMass) }))
|
||||
|
||||
if (lethalMass.num > 0) {
|
||||
warnings.push({
|
||||
code: GeneticsWarningCode.ScheckeLethal,
|
||||
detail: {
|
||||
youngLostFraction: toString(lethalMass),
|
||||
youngLostPercent: Number(((lethalMass.num / lethalMass.den) * 100).toFixed(2)),
|
||||
},
|
||||
})
|
||||
// One lethal warning PER lethal rule that actually removed young (so SpSp ->
|
||||
// ScheckeLethal and S(l)S(l) -> SlsLethal are reported distinctly).
|
||||
for (const rule of LETHAL_RULES) {
|
||||
if (rule.kind !== 'lethal') continue
|
||||
const mass = dist.reduce<Fraction>(
|
||||
(acc, e) => (isHomozygous(e.value, rule.locus, rule.allele) ? add(acc, e.probability) : acc),
|
||||
ZERO,
|
||||
)
|
||||
if (mass.num > 0) {
|
||||
warnings.push({
|
||||
code: rule.warning,
|
||||
detail: {
|
||||
youngLostFraction: toString(mass),
|
||||
youngLostPercent: Number(((mass.num / mass.den) * 100).toFixed(2)),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Semi-lethal: warn if any surviving genotype is homozygous for a semi-lethal allele.
|
||||
@@ -95,5 +105,22 @@ export function applyLethality(dist: DistEntry<Genotype>[]): LethalityResult {
|
||||
}
|
||||
}
|
||||
|
||||
// Superschecke: surviving young carrying BOTH spotting markers (Sp present and
|
||||
// S(l) present) are very-high-white and deafness-prone — info warning.
|
||||
const superMass = distribution.reduce<Fraction>((acc, e) => {
|
||||
const hasSp = e.value.Sp.includes('Sp')
|
||||
const hasSl = e.value.Sls.includes('Sl')
|
||||
return hasSp && hasSl ? add(acc, e.probability) : acc
|
||||
}, ZERO)
|
||||
if (superMass.num > 0) {
|
||||
warnings.push({
|
||||
code: GeneticsWarningCode.SuperscheckeDeaf,
|
||||
detail: {
|
||||
affectedFraction: toString(superMass),
|
||||
affectedPercent: Number(((superMass.num / superMass.den) * 100).toFixed(2)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return { distribution, warnings }
|
||||
}
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
* - de.wikibooks.org/wiki/Die_Rennmaus/_Farbvarianten_und_Farbgenetik
|
||||
*/
|
||||
|
||||
/** Canonical locus keys, in conventional display order. */
|
||||
export const LOCUS_ORDER = ['A', 'C', 'D', 'E', 'G', 'P', 'Sp', 'Re'] as const
|
||||
/**
|
||||
* Canonical locus keys, in conventional display order. Sls (second spotting
|
||||
* locus) is appended LAST so legacy 8-locus genotype strings still parse — a
|
||||
* missing Sls token defaults to wild-type sl/sl.
|
||||
*/
|
||||
export const LOCUS_ORDER = ['A', 'C', 'D', 'E', 'G', 'P', 'Sp', 'Re', 'Sls'] as const
|
||||
export type LocusKey = (typeof LOCUS_ORDER)[number]
|
||||
|
||||
export interface LocusDef {
|
||||
@@ -33,9 +37,12 @@ export interface LocusDef {
|
||||
* E = full extension
|
||||
* ef = Schimmel/roan (progressive whitening)
|
||||
* e = Fox (suppresses eumelanin)
|
||||
* Sp/Re are dominant markers, lethal/semi-lethal when homozygous (see lethality.ts):
|
||||
* Sp = Schecke (checkered); checkered animals are always Spsp, SpSp dies in utero.
|
||||
* Re = Rex (curly coat); rex animals are Re-, ReRe is semi-lethal.
|
||||
* Sp/Re/Sls are dominant markers, lethal/semi-lethal when homozygous (see lethality.ts):
|
||||
* Sp = Schecke (checkered); checkered animals are always Spsp, SpSp dies in utero.
|
||||
* Re = Rex (curly coat); rex animals are Re-, ReRe is semi-lethal.
|
||||
* Sls = second spotting locus (S(l), WP/Minimalschecke). S(l)s(l) het = the WP
|
||||
* phenotype; S(l)S(l) homozygous = lethal (Rumpback/megacolon). Sp + Sls
|
||||
* together => Superschecke (very high white, deafness-prone).
|
||||
*/
|
||||
export const LOCI: Readonly<Record<LocusKey, LocusDef>> = {
|
||||
A: { key: 'A', nameDe: 'Agouti', alleles: ['A', 'a'] },
|
||||
@@ -46,6 +53,7 @@ export const LOCI: Readonly<Record<LocusKey, LocusDef>> = {
|
||||
P: { key: 'P', nameDe: 'Rotaugenaufhellung (Pink-Eye)', alleles: ['P', 'p'] },
|
||||
Sp: { key: 'Sp', nameDe: 'Schecke', alleles: ['Sp', 'sp'] },
|
||||
Re: { key: 'Re', nameDe: 'Rex', alleles: ['Re', 're'] },
|
||||
Sls: { key: 'Sls', nameDe: 'Zweite Scheckung (WP)', alleles: ['Sl', 'sl'] },
|
||||
}
|
||||
|
||||
/** Set of all valid allele symbols, longest-first (for maximal-munch parsing). */
|
||||
|
||||
@@ -10,6 +10,10 @@ export const GeneticsWarningCode = {
|
||||
ScheckeLethal: 'SCHECKE_LETHAL',
|
||||
/** Rex × Rex: ReRe is semi-lethal; reduced viability of homozygous young. */
|
||||
RexSemiLethal: 'REX_SEMI_LETHAL',
|
||||
/** WP × WP: S(l)S(l) is prenatal-lethal (Rumpback/megacolon); fewer live young. */
|
||||
SlsLethal: 'SLS_LETHAL',
|
||||
/** Sp + Sls together -> Superschecke: very high white, deafness-prone (info). */
|
||||
SuperscheckeDeaf: 'SUPERSCHECKE_DEAF',
|
||||
} as const
|
||||
|
||||
export type GeneticsWarningCode =
|
||||
|
||||
@@ -625,6 +625,10 @@ export const de = {
|
||||
'Schecke × Schecke: Reinerbige Tiere (SpSp) sterben bereits im Mutterleib — etwa ein Viertel weniger Jungtiere.',
|
||||
REX_SEMI_LETHAL:
|
||||
'Rex × Rex: Reinerbige Tiere (ReRe) sind nur eingeschränkt lebensfähig.',
|
||||
SLS_LETHAL:
|
||||
'WP × WP: Reinerbige Tiere (S(l)S(l)) sterben bereits im Mutterleib (Rumpback) — weniger Jungtiere.',
|
||||
SUPERSCHECKE_DEAF:
|
||||
'Schecke × WP: Superschecken (sehr hoher Weißanteil) sind möglich — erhöhtes Taubheitsrisiko.',
|
||||
},
|
||||
unknownFarbschlag: 'Unbekannter Farbschlag',
|
||||
},
|
||||
|
||||
@@ -227,6 +227,8 @@ def extract_stammbaum(path):
|
||||
"gender": None,
|
||||
"farbschlag": farbschlag,
|
||||
"genotype": genodict,
|
||||
"deaf": genodict.get("deaf"),
|
||||
"tags": genodict.get("tags", []),
|
||||
"breeder": breeder,
|
||||
"zucht": zraw,
|
||||
"parentRefs": [],
|
||||
@@ -251,7 +253,8 @@ def extract_stammbaum(path):
|
||||
animals.append({
|
||||
"id": None, "name": part, "nameVariants": [],
|
||||
"dob": "", "death": "", "gender": None, "farbschlag": "",
|
||||
"genotype": gt.parse(""), "breeder": "", "zucht": zraw,
|
||||
"genotype": gt.parse(""), "deaf": None, "tags": [],
|
||||
"breeder": "", "zucht": zraw,
|
||||
"parentRefs": [], "photos": [], "sourceFiles": [fname],
|
||||
"_gen": gen_of(c), "_col": c, "_row": r, "_file": fname,
|
||||
"_zucht": norm_zucht(zraw),
|
||||
@@ -472,10 +475,17 @@ def _to_int(s):
|
||||
|
||||
|
||||
# ------------------------------------------------------------- stage 2: dedup
|
||||
def _geno_key(genodict):
|
||||
"""Canonical, order-independent key of a genotype's mapped loci — used for conflict
|
||||
detection so Uw==G (and allele ordering) no longer count as a conflict."""
|
||||
m = genodict.get("mapped8locus", {})
|
||||
return "|".join(f"{locus}:{','.join(sorted(m[locus]))}" for locus in sorted(m))
|
||||
|
||||
|
||||
def dedup(animals):
|
||||
"""Merge by normalise(call-name)+DOB, with the canonical Zucht as
|
||||
DISCRIMINATOR (Julian: same name+DOB but different Zucht = different
|
||||
animal). Returns (merged, conflicts, orphans, zucht_splits)."""
|
||||
DISCRIMINATOR (Julian: same name+DOB+Zucht = same animal; different Zucht =
|
||||
different animal). Returns (merged, conflicts, orphans, zucht_splits)."""
|
||||
groups = {}
|
||||
orphans = []
|
||||
for a in animals:
|
||||
@@ -521,19 +531,26 @@ def dedup(animals):
|
||||
photos = list(base["photos"])
|
||||
parent_refs = list(base["parentRefs"])
|
||||
genos = set()
|
||||
geno_keys = set() # GEN-3b: conflict on NORMALIZED genotype (Uw==G) not raw text
|
||||
farb = set()
|
||||
deaths = set()
|
||||
deaf_seen = set()
|
||||
tags_set = set()
|
||||
for a in grp:
|
||||
variants.add(a["name"])
|
||||
files.update(a["sourceFiles"])
|
||||
photos.extend(a["photos"])
|
||||
parent_refs.extend(a["parentRefs"])
|
||||
if a["genotype"]["rawGenotype"]:
|
||||
if a["genotype"]["mapped8locus"]:
|
||||
genos.add(a["genotype"]["rawGenotype"])
|
||||
geno_keys.add(_geno_key(a["genotype"]))
|
||||
if a["farbschlag"]:
|
||||
farb.add(a["farbschlag"])
|
||||
if a["death"]:
|
||||
deaths.add(norm_dob(a["death"]))
|
||||
if a.get("deaf") is not None:
|
||||
deaf_seen.add(a["deaf"])
|
||||
tags_set.update(a.get("tags", []))
|
||||
# pick the richest genotype (most mapped loci, then longest raw)
|
||||
best = max((a["genotype"] for a in grp),
|
||||
key=lambda gd: (len(gd["mapped8locus"]), len(gd["rawGenotype"])))
|
||||
@@ -554,13 +571,16 @@ def dedup(animals):
|
||||
"photos": sorted(set(photos)),
|
||||
"sourceFiles": sorted(files),
|
||||
"mentions": len(grp),
|
||||
# GEN-3b: hearing/deaf phenotype flag (deaf wins if any mention says so) + tags.
|
||||
"deaf": (True if True in deaf_seen else (False if False in deaf_seen else None)),
|
||||
"tags": sorted(tags_set),
|
||||
# FEAT-8c: machine-readable quarantine marker so the API loader can skip
|
||||
# conflicting records without parsing the German review report.
|
||||
"conflict": False,
|
||||
}
|
||||
merged.append(out)
|
||||
# conflict: same animal, disagreeing genotype or farbschlag or death
|
||||
if len(genos) > 1 or len(farb) > 1 or len(deaths) > 1:
|
||||
# conflict: same animal, disagreeing NORMALIZED genotype (Uw==G) or farbschlag or death
|
||||
if len(geno_keys) > 1 or len(farb) > 1 or len(deaths) > 1:
|
||||
out["conflict"] = True
|
||||
conflicts.append({
|
||||
"id": out["id"], "name": base["name"], "dob": out["dob"],
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
"""Parse the breeder's free-text genotype notation into our frozen 8-locus
|
||||
contract while losing nothing (FEAT-8b ruling from god):
|
||||
"""Parse the breeder's free-text genotype notation into our locus model while
|
||||
losing nothing (FEAT-8b + GEN-3b normalization, per hive/agents/god/GENETIK-notation.md):
|
||||
|
||||
- mapped8locus : {locus: [allele1, allele2]} for A C D E G P Sp Re
|
||||
- rawGenotype : the verbatim source string
|
||||
- unmappedTokens: tokens we couldn't map (Uw/Sls/Dea, markers like WFNZ/WP/DP, …)
|
||||
- mapped8locus : {locus: [allele1, allele2]} for A C D E G P Sp Re (+ Sls when present)
|
||||
- rawGenotype : the verbatim source string
|
||||
- unmappedTokens: tokens we still couldn't place
|
||||
- deaf : True (dea/taub) | False (Dea/hörend) | None (not stated) — phenotype FLAG, not a locus
|
||||
- tags : provenance/breeding markers (WFNZ/RV/GV/DP/extern …) — never genotype
|
||||
|
||||
GEN-3b normalizations (wife + research confirmed):
|
||||
- Uw/uw == G/g (international vs German notation for the SAME locus) -> aliased to G/g.
|
||||
- Sls/WP is a SECOND spotting locus (S(l)s(l) = WP/Minimalschecke het). WP -> Sls het.
|
||||
- Dea/dea/taub -> hearing/deaf flag (written after spsp), NOT a Punnett locus.
|
||||
- WFNZ/RV/GV -> provenance/breeding tags, NOT genotype, NOT conflict-bearing.
|
||||
|
||||
Conventions in the source data:
|
||||
- allele superscripts are bracketed: c[chm] -> c^chm, c[h] -> c^h, e[f] -> e^f
|
||||
- a single '-' for the second allele means "unknown" -> mapped to '?'
|
||||
(frozen-contract wildcard; assumption pending the wife's confirmation)
|
||||
"""
|
||||
import re
|
||||
|
||||
LOCI = ["A", "C", "D", "E", "G", "P", "Sp", "Re"]
|
||||
|
||||
# locus -> regex that matches that locus's token (longest alternatives first)
|
||||
_LOCUS_TOKEN = {
|
||||
"Sp": re.compile(r"^(Sp|sp)(Sp|sp|-)?$"),
|
||||
"Re": re.compile(r"^(Re|re)(Re|re|-)?$"),
|
||||
@@ -25,12 +31,7 @@ _LOCUS_TOKEN = {
|
||||
"G": re.compile(r"^(G|g)(G|g|-)?$"),
|
||||
"P": re.compile(r"^(P|p)(P|p|-)?$"),
|
||||
}
|
||||
# loci our model does NOT have but the data uses
|
||||
_KNOWN_UNMAPPED = re.compile(r"^(Uw|uw)(\[d\])?(Uw|uw)?(\[d\])?$|^(Sls|sls|Dea|dea)$", re.I)
|
||||
_MARKER = re.compile(r"^\[?(WFNZ|WP|DP|GV|RV)\]?$|^\((taub|hörend|hoerend|RV|GV|extern[^)]*)\)$", re.I)
|
||||
|
||||
|
||||
# one allele unit per locus (longest-match alternatives first); '-' = unknown
|
||||
_ALLELE_UNIT = {
|
||||
"Sp": re.compile(r"Sp|sp|-"),
|
||||
"Re": re.compile(r"Re|re|-"),
|
||||
@@ -42,10 +43,41 @@ _ALLELE_UNIT = {
|
||||
"P": re.compile(r"[Pp]|-"),
|
||||
}
|
||||
|
||||
# Provenance/breeding tags (never genotype): Wildfangnachzucht, Rückverpaarung,
|
||||
# Geschwisterverpaarung, DarkPatch, external origin.
|
||||
_TAG = re.compile(r"^\[?(WFNZ|RV|GV|DP)\]?$|^\((RV|GV|extern[^)]*)\)$", re.I)
|
||||
|
||||
|
||||
def _rewrite_uw(token):
|
||||
"""Uw/uw notation -> G/g (same locus). 'Uwuw[d]' -> 'Gg', 'UwUw' -> 'GG', 'uw[d]uw[d]' -> 'gg'."""
|
||||
if "uw" not in token.lower():
|
||||
return token
|
||||
return token.replace("uw[d]", "g").replace("Uw", "G").replace("uw", "g")
|
||||
|
||||
|
||||
def _sls_alleles(token):
|
||||
"""Sls (second spotting locus) alleles, or None. WP == Sls het (Minimalschecke);
|
||||
S(l)S(l) homozygous = lethal. Allele symbols: 'Sl' / 'sl'."""
|
||||
n = token.strip("[]").replace("(l)", "l").replace("(L)", "l")
|
||||
if n in ("WP", "Sls"):
|
||||
return ["Sl", "sl"] # heterozygous (WP phenotype)
|
||||
if n.lower() == "sls":
|
||||
return ["sl", "sl"] # wild-type (no extra spotting)
|
||||
units = re.findall(r"Sl|sl", n)
|
||||
return units if len(units) == 2 else None
|
||||
|
||||
|
||||
def _deaf_value(token):
|
||||
"""dea/taub -> True (deaf); Dea/hörend -> False (hearing); else None. Case-sensitive for Dea/dea."""
|
||||
t = token.strip("()[]")
|
||||
if t == "dea" or t.lower() == "taub":
|
||||
return True
|
||||
if t == "Dea" or t.lower() in ("hörend", "hoerend"):
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _alleles_for(locus, token):
|
||||
"""Extract the (allele1, allele2) pair from a single locus token, handling
|
||||
two-letter alleles (Sp/Re) and bracketed superscripts (c[chm] -> c^chm)."""
|
||||
pat = _ALLELE_UNIT.get(locus)
|
||||
units = pat.findall(token) if pat else re.findall(r"[A-Za-z](?:\[[a-z]+\])?|-", token)
|
||||
alleles = []
|
||||
@@ -55,7 +87,6 @@ def _alleles_for(locus, token):
|
||||
else:
|
||||
m = re.match(r"([A-Za-z]+)\[([a-z\-]+)\]", u)
|
||||
if m:
|
||||
# [-] = sub-allele unknown -> keep the base letter only
|
||||
alleles.append(m.group(1) if m.group(2) == "-" else f"{m.group(1)}^{m.group(2)}")
|
||||
else:
|
||||
alleles.append(u)
|
||||
@@ -65,37 +96,60 @@ def _alleles_for(locus, token):
|
||||
|
||||
|
||||
def parse(raw):
|
||||
"""raw: a genotype string (may include trailing free text/markers).
|
||||
"""raw: a genotype string (may include trailing markers/flags).
|
||||
|
||||
Returns dict {mapped8locus, rawGenotype, unmappedTokens}.
|
||||
Returns {mapped8locus, rawGenotype, unmappedTokens, deaf, tags}.
|
||||
"""
|
||||
raw = (raw or "").strip()
|
||||
mapped = {}
|
||||
unmapped = []
|
||||
# tokenise on whitespace; keep order
|
||||
tags = []
|
||||
deaf = None
|
||||
|
||||
for tok in raw.split():
|
||||
t = tok.strip().rstrip(",")
|
||||
if not t:
|
||||
continue
|
||||
|
||||
# GEN-3b: Uw/uw is an alias of the G locus — rewrite before matching.
|
||||
t = _rewrite_uw(t)
|
||||
|
||||
# 8 standard loci
|
||||
matched = False
|
||||
for locus in LOCI:
|
||||
pat = _LOCUS_TOKEN.get(locus)
|
||||
if pat and pat.match(t):
|
||||
if locus not in mapped: # first occurrence wins
|
||||
mapped[locus] = _alleles_for(locus, t)
|
||||
mapped.setdefault(locus, _alleles_for(locus, t)) # first occurrence wins
|
||||
matched = True
|
||||
break
|
||||
if matched:
|
||||
continue
|
||||
if _KNOWN_UNMAPPED.match(t) or _MARKER.match(t):
|
||||
unmapped.append(t)
|
||||
else:
|
||||
# anything else (stray notes, malformed tokens) -> unmapped, nothing lost
|
||||
unmapped.append(t)
|
||||
|
||||
# Sls (second spotting locus); WP is its heterozygous phenotype
|
||||
sls = _sls_alleles(t)
|
||||
if sls is not None:
|
||||
mapped.setdefault("Sls", sls)
|
||||
continue
|
||||
|
||||
# deafness flag (after spsp): dea/taub vs Dea/hörend
|
||||
d = _deaf_value(t)
|
||||
if d is not None:
|
||||
deaf = d
|
||||
continue
|
||||
|
||||
# provenance/breeding tags
|
||||
if _TAG.match(t):
|
||||
tags.append(re.sub(r"[()\[\]]", "", t).upper())
|
||||
continue
|
||||
|
||||
unmapped.append(t)
|
||||
|
||||
return {
|
||||
"mapped8locus": mapped,
|
||||
"rawGenotype": raw,
|
||||
"unmappedTokens": unmapped,
|
||||
"deaf": deaf,
|
||||
"tags": tags,
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +157,7 @@ def looks_like_genotype(text):
|
||||
"""Heuristic: does this cell text contain >=3 recognisable locus tokens?"""
|
||||
n = 0
|
||||
for tok in text.split():
|
||||
t = tok.rstrip(",")
|
||||
t = _rewrite_uw(tok.rstrip(","))
|
||||
if any(p.match(t) for p in _LOCUS_TOKEN.values()):
|
||||
n += 1
|
||||
return n >= 3
|
||||
|
||||
@@ -4,15 +4,15 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date
|
||||
|
||||
## Überblick
|
||||
|
||||
- Rohe Tier-Einträge aus den Stammbäumen: **889**
|
||||
- Nach Zusammenführung (eindeutige Tiere): **574**
|
||||
- davon mit Geburtsdatum: 279
|
||||
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 146
|
||||
- Konflikte zur Klärung: **32**
|
||||
- Rohe Tier-Einträge aus den Stammbäumen: **950**
|
||||
- Nach Zusammenführung (eindeutige Tiere): **622**
|
||||
- davon mit Geburtsdatum: 327
|
||||
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 158
|
||||
- Konflikte zur Klärung: **27**
|
||||
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **310**
|
||||
- Fotos zugeordnet: **123**
|
||||
- Fotos zugeordnet: **137**
|
||||
- Würfe aus der Wurfchronik: **752**
|
||||
- Tiere mit Wurf verknüpft: **135** (davon über Geburtsdatum **und** Eltern: 95, nur über Geburtsdatum: 40; mehrdeutig: 9)
|
||||
- Tiere mit Wurf verknüpft: **159** (davon über Geburtsdatum **und** Eltern: 110, nur über Geburtsdatum: 49; mehrdeutig: 10)
|
||||
- Würfe mit Datenqualitäts-Hinweisen: 113 (+ 138 Zeilen mit abweichendem Spaltenschema)
|
||||
|
||||
## Zusammenführungs-Schlüssel
|
||||
@@ -28,19 +28,15 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
||||
| Ella | 10.06.2019 | Aa C D- ee[f] GG P- spsp // Aa Cc[chm] D- ee[f] UwUw P- spsp | Algierfuchsschimmel, hell | 03.02.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
|
||||
| ZoneFire | 07.12.2020 | Aa c[chm]c[chm] D- Ee Gg P- Spsp | CP-Agouti Kragenschecke // Kalea von den Kleinen Chaoten | — | Stammbaum von Akio Kids |
|
||||
| Louis von den Kleinen Chaoten | 15.07.2017 | Aa Cc[] D- Ee Gg P- spsp // Aa Cc[chm] D- Ee Uwuw[d] P- spsp | Roswitha von den Kleinen Chaoten | 01.07.2020 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
||||
| Roswitha von den Kleinen Chaoten | 10.09.2018 | aa CC D- ee[f] Gg P- spsp // aa CC D- ee[f] Uwuw[d] P- spsp | — | 05.08.2021 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
||||
| Firefly von den Kleinen Chaoten | 18.12.2019 | /+, Aa c[chm]c[chm] D- Ee Gg PP Spsp // Aa c[chm]c[chm] DD Ee Gg PP Spsp | — | 2024 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
|
||||
| Zuleika von den Kleinen Chaoten | 24.10.2015 | aa c[chm]c[h] D- E G P- spsp // aa c[chm]c[h] D- Ee Gg P- spsp // aa c[chm]c[h] DD Ee Gg P- spsp | — | 24.02.2019 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
|
||||
| WildFire von den Kleinen Chaoten | 05.10.2017 | aa c[chm]c[chm] D- Ee gg P- spsp // aa c[chm]c[chm] D- Ee gg PP spsp | — | — | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
|
||||
| Vestra von den Schlossmäusen | 08.02.2019 | Aa Cc[chm] D- EE GG PP Spsp [WP] // Aa Cc[chm] DD EE GG PP Spsp [WP] | — | 26.05.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids |
|
||||
| Flint von den Kleinen Chaoten | 23.12.2017 | aa Cc[chm] D- ee Gg P- spsp | — | 10.05.2021 // 10.05.2022 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
||||
| Silenos gen. Adonis v.d. Kleinen Chaoten | 11.10.2015 | aa Cc[chm] D- Ee Gg PP spsp // aa Cc[chm] D- Ee Uwuw[d] PP spsp | — | 18.07.2019 | Stammbaum von Akio Kids, Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Watarus Kids |
|
||||
| Kazu von den Kleinen Chaoten | 23.04.2013 | Aa Cc[chm] DD e[f]e[f] Gg P Spsp // Aa Cc[chm] DD ee[f] UwUw PP Spsp | — | 03.09.2017 | Stammbaum von Akio Kids, Stammbaum von Vance |
|
||||
| Bruno of Black Forest | 01.06.2022 | aa C- dd Ee Gg P- spsp | Blau // Mystique of Black Forest | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids |
|
||||
| Milka of LennyLengo | 09.12.2018 | aa C- dd E- Gg P- Spsp // aa Cc[h] dd EE Gg P- Spsp | — | 22.12.2021 | Stammbaum von Alberto Kids, Stammbaum von Stella Kids |
|
||||
| Hedwig of BGB | 30.10.2019 | aa CC DD E- G- P- Spsp WP // aa CC DD E- G- P- Spsp WP DP (hörend) | — | 30.08.2023 | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids |
|
||||
| Silvain von den Kleinen Chaoten | 27.03.2022 | aa c[chm]c[chm] Dd Ee[-] Gg P- Spsp // aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp | — | 31.12.2024 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
||||
| Pitari gen. Piti von den Kleinen Chaoten | 16.05.2021 | Aa CC dd ee Gg P- Spsp DP // Aa CC dd ee Gg P- Spsp [DP] | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids |
|
||||
| Brandon Stark von den Kleinen Chaoten | 13.12.2017 | aa Cc[chm] D- Ee Gg P- spsp // aa Cc[chm] D- Ee Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids |
|
||||
| Enya von den Kleinen Chaoten | 01.11.2017 | Aa c[chm]c[chm] D- ee[-] G- P- spsp // Aa c[chm]c[chm] D- ee[-] Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids |
|
||||
| Little Hero of Black Forest | 22.02.2018 | AA CC DD EE GG PP [WFNZ] // AA CC DD EE GG PP spsp [WFNZ] | — | 18.06.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids, Stammbaum von Valentino Firehearts Kids |
|
||||
| Molly of Black Forest | 13.09.2021 | /+, Aa Cc[chm] D- Ee gg P- spsp // Aa Cc[chm] Dd Ee gg Pp spsp | — | 03.05.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity |
|
||||
@@ -52,11 +48,10 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
||||
| Chayton v.d. Kleinen Chaoten (extern SC) | 04.02.2022 | aa Cc[-] D- e[f]e[f] Gg Pp spsp | Orangeschimmel, hell // Victoria Welby gen. Welby v.d. Kleinen Chaoten | 30.04.2024 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
||||
| Victoria Welby gen. Welby v.d. Kleinen Chaoten | 16.01.2023 | Aa CC D- Ee[f] Gg pp Spsp [DP] // Aa CC D- ee[f] Gg pp Spsp [DP] | Goldfuchsschimmel Punktschecke DP | 17.02.2026 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
||||
| Zac gen. Action von den Kleinen Chaoten | 25.12.2020 | aa C- D- Ee G- Pp Spsp [DP] // aa CC D- Ee G- Pp Spsp [DP] | Belica gen. Emi von den Kleinen Chaoten | 31.01.2025 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
||||
| Chelsea von den Kleinen Chaoten | 02.04.2021 | /+, Aa CC Dd ee gg Pp spsp // Aa CC Dd ee gg Pp spsp | — | — | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Valentino Firehearts Kids |
|
||||
| Chesnut | 13.11.2019 | aa C- D- ee[f] GG PP spsp | Kohlfuchsschimmel // Tennessee von den Kleinen Chaoten | 22.11.2023 | Stammbaum von Kentucky |
|
||||
| Ethan von den Kleinen Chaoten | 09.07.2020 | Aa Cc[chm] D- ee[f] Gg Pp Spsp | Ichika von den Kleinen Chaoten // Orangeschimmel, hell Kragenschecke | 30.07.2024 | Stammbaum von Kentucky, Stammbaum von Watarus Kids |
|
||||
| Quied Soldier of Black Forest | 07.06.2018 | /+, Aa C- D- ee[f] GG Pp Spsp [DP] // Aa C- D- ee[f] GG Pp Spsp DP | Hoshi von den Kleinen Chaoten | — | Stammbaum von Kentucky |
|
||||
| Hanami von den Kleinen Chaoten | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp | — | 12.12.2019 // 14.01.2020 | Stammbaum von Kentucky, Stammbaum von Stella Kids |
|
||||
| Skarlett v.d. Kleinen Chaoten | 14.07.2013 | / +2018, Aa Cc[chm] DD ee uw[d]uw[d] PP spsp // Aa Cc[chm] DD ee uw[d]uw[d] PP spsp | — | 17.04.2016 // 2018 | Stammbaum von Vance |
|
||||
|
||||
## Mehrdeutige / unvollständige Einträge
|
||||
|
||||
@@ -105,7 +100,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
||||
|
||||
## Wahrscheinliche Zuordnungen unvollständiger Einträge
|
||||
|
||||
38 namenlose/datenlose Einträge tragen denselben Namen wie ein vollständiges Tier — vermutlich dasselbe Tier (zur Bestätigung):
|
||||
39 namenlose/datenlose Einträge tragen denselben Namen wie ein vollständiges Tier — vermutlich dasselbe Tier (zur Bestätigung):
|
||||
|
||||
- „Oscar of Black Forest“ → Oscar of Black Forest (*12.06.2019)
|
||||
- „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019)
|
||||
@@ -137,6 +132,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
||||
- „Zadar from Zeko i ptica, Croatia“ → Zadar from Zeko i ptica, Croatia (*12.04.2019)
|
||||
- „Living Force's Vally“ → Living Force's Vally (*01.11.2014)
|
||||
- „Pinto of Fiomi“ → Pinto of Fiomi (*28.08.2016)
|
||||
- „Hanse Renner's Poseidon“ → Hanse Renner's Poseidon (*14.08.2014)
|
||||
- „Oscar of Black Forest“ → Oscar of Black Forest (*12.06.2019)
|
||||
- „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019)
|
||||
- „Lilo of LennyLengo“ → Lilo of LennyLengo (*04.11.2018)
|
||||
@@ -152,29 +148,21 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M
|
||||
|
||||
| Token | Vorkommen | Bedeutung (Vermutung) |
|
||||
|---|---|---|
|
||||
| `[DP]` | 15 | Marker (Dunkelpigment?) |
|
||||
| `[WFNZ]` | 13 | Marker |
|
||||
| `DP` | 9 | Marker |
|
||||
| `/+` | 8 | ? |
|
||||
| `WP` | 7 | Marker |
|
||||
| `Uwuw[d]` | 4 | 9. Locus Uw (nicht im Modell) |
|
||||
| `-g` | 2 | ? |
|
||||
| `C(C)` | 2 | Schreibweise (C trägt c) |
|
||||
| `[WP]` | 2 | Marker |
|
||||
| `chmchm` | 2 | Schreibweise (c[chm]c[chm]) |
|
||||
| `Cc[]` | 1 | ? |
|
||||
| `-psp` | 1 | ? |
|
||||
| `G(G)` | 1 | ? |
|
||||
| `UwUw` | 1 | 9. Locus Uw |
|
||||
| `uw[d]uw[d]` | 1 | ? |
|
||||
| `[DP` | 1 | ? |
|
||||
| `/` | 1 | ? |
|
||||
| `+2018` | 1 | ? |
|
||||
| `c[chm]chm]` | 1 | ? |
|
||||
| `Dea/dea]` | 1 | ? |
|
||||
| `DD-Tumor` | 1 | ? |
|
||||
| `bei` | 1 | ? |
|
||||
| `Geschwistern` | 1 | ? |
|
||||
| `C-D-` | 1 | ? |
|
||||
| `Sls` | 1 | ? |
|
||||
| `(hörend)` | 1 | ? |
|
||||
| `-DD` | 1 | ? |
|
||||
|
||||
## Wurfchronik — Datenqualitäts-Hinweise
|
||||
|
||||
71
tools/import/test_genotype.py
Normal file
71
tools/import/test_genotype.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Zero-dep tests for genotype.py GEN-3b normalization.
|
||||
|
||||
Run: python test_genotype.py (exit 0 = all pass)
|
||||
Covers: Uw/uw -> G/g alias, Sls/WP second spotting locus, dea/Dea/taub
|
||||
hearing-deaf flag, WFNZ/RV/GV/DP provenance tags. Per hive/agents/god/GENETIK-notation.md.
|
||||
"""
|
||||
import sys
|
||||
import genotype as g
|
||||
|
||||
|
||||
def check(name, cond):
|
||||
if not cond:
|
||||
print(f"FAIL: {name}")
|
||||
check.failed += 1
|
||||
else:
|
||||
print(f"ok: {name}")
|
||||
check.failed = 0
|
||||
|
||||
|
||||
# --- Uw/uw == G/g (same locus) ---
|
||||
r = g.parse("aa Cc Dd Ee Uwuw Pp spsp rere")
|
||||
check("Uw->G: G locus mapped", r["mapped8locus"].get("G") == ["G", "g"])
|
||||
check("Uw->G: nothing left in unmapped", r["unmappedTokens"] == [])
|
||||
|
||||
r = g.parse("UwUw")
|
||||
check("UwUw -> GG", r["mapped8locus"].get("G") == ["G", "G"])
|
||||
r = g.parse("uwuw")
|
||||
check("uwuw -> gg", r["mapped8locus"].get("G") == ["g", "g"])
|
||||
r = g.parse("uw[d]uw[d]")
|
||||
check("uw[d]uw[d] -> gg (dense underwhite)", r["mapped8locus"].get("G") == ["g", "g"])
|
||||
|
||||
# Gg and Uwuw must produce the SAME mapped locus (so they stop being a conflict)
|
||||
check("Gg identical to Uwuw at G locus",
|
||||
g.parse("Gg")["mapped8locus"]["G"] == g.parse("Uwuw")["mapped8locus"]["G"])
|
||||
|
||||
# --- Sls / WP second spotting locus ---
|
||||
check("WP -> Sls het", g.parse("WP")["mapped8locus"].get("Sls") == ["Sl", "sl"])
|
||||
check("[WP] (bracketed) -> Sls het", g.parse("[WP]")["mapped8locus"].get("Sls") == ["Sl", "sl"])
|
||||
check("Sls token -> Sls het", g.parse("Sls")["mapped8locus"].get("Sls") == ["Sl", "sl"])
|
||||
check("sls -> Sls wild", g.parse("sls")["mapped8locus"].get("Sls") == ["sl", "sl"])
|
||||
check("S(l)s(l) -> Sl,sl", g.parse("S(l)s(l)")["mapped8locus"].get("Sls") == ["Sl", "sl"])
|
||||
# Sp and Sls are TWO distinct loci on the same animal (Superschecke)
|
||||
r = g.parse("spsp WP")
|
||||
check("Sp + Sls coexist (two spotting loci)",
|
||||
r["mapped8locus"].get("Sp") == ["sp", "sp"] and r["mapped8locus"].get("Sls") == ["Sl", "sl"])
|
||||
|
||||
# --- deafness flag (after spsp), case-sensitive ---
|
||||
check("dea (lower) -> deaf True", g.parse("spsp dea")["deaf"] is True)
|
||||
check("taub -> deaf True", g.parse("taub")["deaf"] is True)
|
||||
check("Dea (upper) -> hearing False", g.parse("spsp Dea")["deaf"] is False)
|
||||
check("(hörend) -> hearing False", g.parse("(hörend)")["deaf"] is False)
|
||||
check("no deaf token -> None", g.parse("aa Cc")["deaf"] is None)
|
||||
# deafness is NOT a genotype locus and must not pollute mapped/unmapped silently
|
||||
check("deaf flag not in unmapped", "dea" not in g.parse("spsp dea")["unmappedTokens"])
|
||||
|
||||
# --- provenance / breeding tags (never genotype, never conflict) ---
|
||||
check("WFNZ -> tag", g.parse("aa WFNZ")["tags"] == ["WFNZ"])
|
||||
check("RV -> tag", g.parse("(RV)")["tags"] == ["RV"])
|
||||
check("GV -> tag", g.parse("(GV)")["tags"] == ["GV"])
|
||||
check("DP -> tag", g.parse("[DP]")["tags"] == ["DP"])
|
||||
check("tag not in genotype loci", g.parse("WFNZ")["mapped8locus"] == {})
|
||||
check("tag not in unmapped", g.parse("aa WFNZ")["unmappedTokens"] == [])
|
||||
|
||||
# --- looks_like_genotype recognizes Uw-bearing cells ---
|
||||
check("looks_like_genotype sees Uw as G",
|
||||
g.looks_like_genotype("aa Cc Uwuw") is True)
|
||||
|
||||
if check.failed:
|
||||
print(f"\n{check.failed} test(s) FAILED")
|
||||
sys.exit(1)
|
||||
print("\nALL PASS")
|
||||
Reference in New Issue
Block a user