Files
GerbilManager/gerbil-manager-web/src/genetics/breed.ts
2026-06-05 23:42:05 +02:00

91 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Probeverpaarung (test mating): predict offspring of two parent genotypes.
*
* Pipeline: Punnett combine -> merge identical genotypes -> apply lethality
* (remove SpSp, renormalise, collect warnings) -> annotate each surviving
* genotype with its Farbschlag and probability. Also aggregates by Farbschlag.
*
* Pure & deterministic; no React, no I/O. Display texts (warning messages) are
* resolved from CODES in the UI layer (de.ts).
*/
import { add, toNumber, toPercentString, toString, ZERO, type Fraction } from './fraction'
import { type Genotype, toDisplayString, toJSON } from './genotype'
import { combineGenotypes, mergeByGenotype } from './punnett'
import { applyLethality } from './lethality'
import { farbschlagFor } from './catalog'
import type { GeneticsWarning } from './warnings'
import type { LocusKey } from './loci'
export interface Probability {
readonly fraction: Fraction
/** Decimal in [0,1]. */
readonly value: number
/** "3/16" style. */
readonly text: string
/** "18.75 %" style. */
readonly percent: string
}
function prob(f: Fraction): Probability {
return { fraction: f, value: toNumber(f), text: toString(f), percent: toPercentString(f) }
}
export interface OffspringGenotype {
readonly farbschlag: string
/** Compact display string, e.g. "Aa CC Dd EE GG Pp spsp rere". */
readonly genotype: string
/** Structured genotype (JSON storage shape). */
readonly genotypeJson: Record<LocusKey, [string, string]>
readonly probability: Probability
/** True when the Farbschlag could not be named from the catalog. */
readonly unknownFarbschlag: boolean
}
export interface FarbschlagShare {
readonly farbschlag: string
readonly probability: Probability
}
export interface BreedingResult {
/** Per-genotype offspring distribution (live births), highest probability first. */
readonly offspring: OffspringGenotype[]
/** Aggregated by Farbschlag (colour-morph), highest probability first. */
readonly byFarbschlag: FarbschlagShare[]
readonly warnings: GeneticsWarning[]
}
function byProbDesc<T extends { probability: Probability }>(a: T, b: T): number {
return b.probability.value - a.probability.value
}
/** Predict the offspring distribution of father × mother. */
export function breed(father: Genotype, mother: Genotype): BreedingResult {
const raw = combineGenotypes(father, mother)
const merged = mergeByGenotype(raw)
const { distribution, warnings } = applyLethality(merged)
const offspring: OffspringGenotype[] = distribution
.map((entry) => {
const match = farbschlagFor(entry.value)
return {
farbschlag: match.name,
genotype: toDisplayString(entry.value),
genotypeJson: toJSON(entry.value),
probability: prob(entry.probability),
unknownFarbschlag: match.unknown,
}
})
.sort(byProbDesc)
const colorMass = new Map<string, Fraction>()
for (const entry of distribution) {
const name = farbschlagFor(entry.value).name
colorMass.set(name, add(colorMass.get(name) ?? ZERO, entry.probability))
}
const byFarbschlag: FarbschlagShare[] = Array.from(colorMass.entries())
.map(([farbschlag, f]) => ({ farbschlag, probability: prob(f) }))
.sort(byProbDesc)
return { offspring, byFarbschlag, warnings }
}