dedup: 'presence wins' — present-vs-absent token is not a conflict (Julian)
Breeder merge rule: when two source variants of the SAME animal differ ONLY by a token PRESENT in one and ABSENT in the other — a whole locus (e.g. spsp charted in one source, omitted in another) or a modifier on the same base allele (e^f vs e, the [f] marker) — keep the present token; that is NOT a conflict. Genuine VALUE contradictions still quarantine: different base alleles (Ee↔ee), unknown-vs-filled (D-↔DD), different modifiers (c[h]↔c[chm]), C-↔Cc[h], P-↔Pp. Replaces the old `len(distinct normalized geno keys) > 1` test with _genotype_conflict() (per-locus, per-allele compatibility; '?'-vs-filled is a contradiction, modifier-present-vs-absent and whole-locus-absence are not). Markers/flags (WP/DP/WFNZ/hörend) are already tags/flags, never genotype, so they never reach conflict detection; empty Farbschlag/death already don't conflict (only non-empty values are compared). Clears Daja (keep spsp), Ichika (keep ee[f]) and the D4 marker cases: Konflikte 19 -> 15. test_extract covers spsp/[f] present-vs-absent = no conflict and the four genuine-contradiction shapes. python + dotnet 121/121 green; extractor-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -502,6 +502,52 @@ def _geno_key(genodict):
|
||||
return "|".join(f"{locus}:{','.join(sorted(m[locus]))}" for locus in sorted(m))
|
||||
|
||||
|
||||
# --- "presence wins" merge rule (Julian) -------------------------------------
|
||||
# When two source variants of the SAME animal differ ONLY by a token PRESENT in one and
|
||||
# ABSENT in the other — a whole locus (e.g. spsp recorded in one chart, omitted in another)
|
||||
# or a modifier on the same base allele (e^f vs e, i.e. the [f] marker) — keep the present
|
||||
# token; that is NOT a conflict. A genuine VALUE contradiction (different filled alleles:
|
||||
# E vs e, D vs d, c^h vs c^chm) OR unknown-vs-filled (D- vs DD, the '?' second allele) STILL
|
||||
# quarantines for human decision. (Markers/flags WP/DP/WFNZ/hörend are already tags/flags,
|
||||
# never part of the genotype, so they never reach here.)
|
||||
def _split_allele(a):
|
||||
return tuple(a.split("^", 1)) if "^" in a else (a, "")
|
||||
|
||||
|
||||
def _alleles_compatible(a, b):
|
||||
if a == b:
|
||||
return True
|
||||
if a == "?" or b == "?":
|
||||
return False # unknown vs filled = contradiction (D- vs DD)
|
||||
(ba, ma), (bb, mb) = _split_allele(a), _split_allele(b)
|
||||
if ba != bb:
|
||||
return False # different base allele = real value diff (E vs e)
|
||||
return ma == "" or mb == "" # same base, modifier present-vs-absent -> presence wins
|
||||
|
||||
|
||||
def _pair_compatible(p, q):
|
||||
if len(p) != 2 or len(q) != 2:
|
||||
return p == q
|
||||
return ((_alleles_compatible(p[0], q[0]) and _alleles_compatible(p[1], q[1])) or
|
||||
(_alleles_compatible(p[0], q[1]) and _alleles_compatible(p[1], q[0])))
|
||||
|
||||
|
||||
def _genotype_conflict(mapped_list):
|
||||
"""True only if two variants GENUINELY contradict at a shared locus. A locus present in
|
||||
one variant and absent in another is fine (presence wins); so is a modifier present-vs-
|
||||
absent on the same base allele. Replaces the old `len(distinct geno keys) > 1` test."""
|
||||
loci = set()
|
||||
for m in mapped_list:
|
||||
loci.update(m.keys())
|
||||
for locus in loci:
|
||||
pairs = [m[locus] for m in mapped_list if locus in m]
|
||||
for i in range(len(pairs)):
|
||||
for j in range(i + 1, len(pairs)):
|
||||
if not _pair_compatible(pairs[i], pairs[j]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def dedup(animals):
|
||||
"""Merge by normalise(call-name)+DOB, with the canonical Zucht as
|
||||
DISCRIMINATOR (Julian: same name+DOB+Zucht = same animal; different Zucht =
|
||||
@@ -552,6 +598,7 @@ def dedup(animals):
|
||||
parent_refs = list(base["parentRefs"])
|
||||
genos = set()
|
||||
geno_keys = set() # GEN-3b: conflict on NORMALIZED genotype (Uw==G) not raw text
|
||||
mapped_variants = [] # mapped8locus per variant — for the 'presence wins' conflict test
|
||||
farb = set()
|
||||
deaths = set()
|
||||
deaf_seen = set()
|
||||
@@ -567,6 +614,7 @@ def dedup(animals):
|
||||
if a["genotype"]["mapped8locus"]:
|
||||
genos.add(a["genotype"]["rawGenotype"])
|
||||
geno_keys.add(_geno_key(a["genotype"]))
|
||||
mapped_variants.append(a["genotype"]["mapped8locus"])
|
||||
if a["farbschlag"]:
|
||||
farb.add(a["farbschlag"])
|
||||
if a["death"]:
|
||||
@@ -603,8 +651,9 @@ def dedup(animals):
|
||||
"conflict": False,
|
||||
}
|
||||
merged.append(out)
|
||||
# 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:
|
||||
# conflict: same animal, GENUINELY disagreeing genotype (presence-vs-absence is NOT a
|
||||
# conflict — Julian's 'presence wins') or >1 distinct farbschlag or >1 distinct death.
|
||||
if _genotype_conflict(mapped_variants) or len(farb) > 1 or len(deaths) > 1:
|
||||
out["conflict"] = True
|
||||
conflicts.append({
|
||||
"id": out["id"], "name": base["name"], "dob": out["dob"],
|
||||
|
||||
Reference in New Issue
Block a user