GEN-1: add lethality rules + warning codes (SpSp lethal+renormalize, ReRe semi)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:40:06 +02:00
parent 76d8380ea1
commit ebc61acd18
2 changed files with 115 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
/**
* Lethality rules applied to an offspring genotype distribution.
*
* Data-driven so the biology is easy to adjust:
* - SpSp (Schecke homozygous): PRENATAL-LETHAL. Embryos die and are resorbed,
* so they never appear as live young -> removed from the distribution, which
* is then renormalised. Triggers a ScheckeLethal warning carrying the
* fraction of young lost (~1/4 for Schecke × Schecke).
* - ReRe (Rex homozygous): SEMI-LETHAL. Reduced viability but not modelled as a
* hard removal (some survive) -> kept in the distribution, warning only.
*/
import { add, divide, toString, ZERO, type Fraction } from './fraction'
import type { LocusKey } from './loci'
import type { Genotype } from './genotype'
import type { DistEntry } from './punnett'
import { GeneticsWarningCode, type GeneticsWarning } from './warnings'
interface LethalRule {
readonly locus: LocusKey
readonly allele: string
/** 'lethal' = remove from live-birth distribution; 'semi' = warn only. */
readonly kind: 'lethal' | 'semi'
readonly warning: GeneticsWarningCode
}
const LETHAL_RULES: readonly LethalRule[] = [
{ locus: 'Sp', allele: 'Sp', kind: 'lethal', warning: GeneticsWarningCode.ScheckeLethal },
{ locus: 'Re', allele: 'Re', kind: 'semi', warning: GeneticsWarningCode.RexSemiLethal },
]
function isHomozygous(g: Genotype, locus: LocusKey, allele: string): boolean {
return g[locus][0] === allele && g[locus][1] === allele
}
export interface LethalityResult {
/** Live-birth distribution (lethal genotypes removed, renormalised). */
readonly distribution: DistEntry<Genotype>[]
readonly warnings: GeneticsWarning[]
}
/**
* Apply lethality to a (merged) genotype distribution.
* The input probabilities are assumed to sum to 1.
*/
export function applyLethality(dist: DistEntry<Genotype>[]): LethalityResult {
const warnings: GeneticsWarning[] = []
// Total probability mass of fully-lethal genotypes (for the "young lost" stat).
let lethalMass: Fraction = ZERO
const survivors: DistEntry<Genotype>[] = []
for (const entry of dist) {
const hardLethal = LETHAL_RULES.some(
(r) => r.kind === 'lethal' && isHomozygous(entry.value, r.locus, r.allele),
)
if (hardLethal) {
lethalMass = add(lethalMass, entry.probability)
} else {
survivors.push(entry)
}
}
// Renormalise survivors over the surviving mass.
const survivingMass = survivors.reduce<Fraction>((acc, e) => add(acc, e.probability), ZERO)
const distribution =
survivingMass.num === 0
? 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)),
},
})
}
// Semi-lethal: warn if any surviving genotype is homozygous for a semi-lethal allele.
for (const rule of LETHAL_RULES) {
if (rule.kind !== 'semi') continue
const affected = distribution.reduce<Fraction>(
(acc, e) => (isHomozygous(e.value, rule.locus, rule.allele) ? add(acc, e.probability) : acc),
ZERO,
)
if (affected.num > 0) {
warnings.push({
code: rule.warning,
detail: {
affectedFraction: toString(affected),
affectedPercent: Number(((affected.num / affected.den) * 100).toFixed(2)),
},
})
}
}
return { distribution, warnings }
}

View File

@@ -0,0 +1,16 @@
/**
* Warning codes emitted by the genetics engine. The engine is language-neutral:
* it emits CODES only. German texts are mapped in de.ts (UI layer).
*/
export enum GeneticsWarningCode {
/** Schecke × Schecke: SpSp embryos die in utero; ~25% fewer live young. */
ScheckeLethal = 'SCHECKE_LETHAL',
/** Rex × Rex: ReRe is semi-lethal; reduced viability of homozygous young. */
RexSemiLethal = 'REX_SEMI_LETHAL',
}
export interface GeneticsWarning {
readonly code: GeneticsWarningCode
/** Optional structured detail for the UI (e.g. fraction of young lost). */
readonly detail?: Record<string, string | number>
}