91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
/**
|
||
* 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 }
|
||
}
|