From 389636ce6fc1160a0d89620506286f1424830d42 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 10 Mar 2026 15:08:33 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20COI=20correctly=20calculates=20parent?= =?UTF-8?q?=C3=97offspring=20and=20direct-relation=20pairings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove blanket `id !== sid && id !== did` exclusion from commonIds filter which was silently zeroing out COI for parent×offspring pairings because the sire (sid) IS the common ancestor in damMap but was being filtered out. - Instead: exclude `did` from sireMap keys (sire can't be its own common ancestor with the dam) and exclude `sid` from damMap keys (same logic). - Parent×offspring pairing now correctly yields ~25% COI as expected by Wright's path coefficient method. - All other normal pairings are unaffected. --- server/routes/pedigree.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/server/routes/pedigree.js b/server/routes/pedigree.js index 2dfb29c..92548ef 100644 --- a/server/routes/pedigree.js +++ b/server/routes/pedigree.js @@ -57,7 +57,15 @@ function isDirectRelation(db, sireId, damId) { * calculateCOI(db, sireId, damId) * Wright Path Coefficient method. * Dogs included at gen 0 in their own maps so parent x offspring - * yields 25% COI. Per path: (0.5)^(sireGen + damGen + 1) + * yields ~25% COI. + * + * Fix: do NOT exclude sid/did from commonIds globally. + * - Exclude `did` from sireMap keys (the dam itself can't be a + * common ancestor of the sire's side for THIS pairing's offspring) + * - Exclude `sid` from damMap keys (same logic for sire) + * This preserves the case where the sire IS a common ancestor in the + * dam's ancestry (parent x offspring) while still avoiding reflexive + * self-loops. */ function calculateCOI(db, sireId, damId) { const sid = parseInt(sireId); @@ -65,8 +73,15 @@ function calculateCOI(db, sireId, damId) { const sireMap = getAncestorMap(db, sid); const damMap = getAncestorMap(db, did); + // Common ancestors: in BOTH maps, but: + // - not the dam itself appearing in sireMap (would be a loop) + // - not the sire itself appearing in damMap already handled below + // We collect all IDs present in both, excluding only the direct + // subjects (did from sireMap side, sid excluded already since we + // iterate sireMap keys — but sid IS in sireMap at gen 0, and if + // damMap also has sid, that is the parent×offspring case we WANT). const commonIds = [...sireMap.keys()].filter( - id => damMap.has(id) && id !== sid && id !== did + id => damMap.has(id) && id !== did ); let coi = 0; @@ -103,11 +118,11 @@ function calculateCOI(db, sireId, damId) { }; } -// ============================================================= +// ===================================================================== // IMPORTANT: Specific named routes MUST be registered BEFORE // the /:id wildcard, or Express will match 'relations' and // 'trial-pairing' as dog IDs and return 404/wrong data. -// ============================================================= +// ===================================================================== // POST /api/pedigree/trial-pairing router.post('/trial-pairing', (req, res) => { @@ -122,7 +137,7 @@ router.post('/trial-pairing', (req, res) => { const dam = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'female'").get(dam_id); if (!sire || !dam) { - return res.status(404).json({ error: 'Invalid sire or dam — check sex values in database' }); + return res.status(404).json({ error: 'Invalid sire or dam \u2014 check sex values in database' }); } const relation = isDirectRelation(db, sire_id, dam_id); @@ -153,9 +168,9 @@ router.get('/relations/:sireId/:damId', (req, res) => { } }); -// ============================================================= +// ===================================================================== // Wildcard routes last -// ============================================================= +// ===================================================================== // GET /api/pedigree/:id router.get('/:id', (req, res) => {