variant-linker
Version:
CLI for Ensembl VEP and Variant Recoder
568 lines (514 loc) • 23.5 kB
JavaScript
// src/inheritance/inheritanceAnalyzer.js
;
/**
* @fileoverview Orchestrates inheritance pattern analysis for variants.
* Integrates pattern deduction, segregation checking, prioritization, and
* compound heterozygous analysis.
* Replaces the old inheritanceCalculator.js module.
* @module inheritanceAnalyzer
*/
const debug = require('debug')('variant-linker:inheritance:analyzer');
const debugDetailed = require('debug')('variant-linker:detailed');
// Import specialized modules
const patternDeducer = require('./patternDeducer');
const segregationChecker = require('./segregationChecker');
const patternPrioritizer = require('./patternPrioritizer');
const compoundHetAnalyzer = require('./compoundHetAnalyzer');
/**
* Determines the index/proband sample ID based on available information.
* Priority:
* 1. Explicit sampleMap with 'index' or 'proband'.
* 2. First affected ('2') sample found in pedigreeData.
* 3. Fallback to the first sample ID found in the first variant's genotype map.
*
* @param {Map<string, Map<string, string>>} genotypesMap - Map of variant keys to genotype maps.
* @param {Map<string, Object>|null} pedigreeData - Optional pedigree data.
* @param {Object|null} sampleMap - Optional sample role mapping.
* @returns {string | null} The determined index sample ID or null if none could be found.
* @private
*/
function _determineIndexSampleId(genotypesMap, pedigreeData, sampleMap) {
debugDetailed(`--- Determining Index Sample ID ---`);
let indexSampleId = null;
// 1. Check sampleMap
if (sampleMap) {
if (sampleMap.index) {
indexSampleId = sampleMap.index;
debugDetailed(` Index ID found in sampleMap.index: ${indexSampleId}`);
} else if (sampleMap.proband) {
// Allow 'proband' as well
indexSampleId = sampleMap.proband;
debugDetailed(` Index ID found in sampleMap.proband: ${indexSampleId}`);
}
}
// 2. Check pedigree for affected status '2' if index not found yet
if (!indexSampleId && pedigreeData && pedigreeData.size > 0) {
debugDetailed(` Checking pedigree for affected status '2'...`);
for (const [sampleId, sampleData] of pedigreeData.entries()) {
if (sampleData.affectedStatus === '2' || sampleData.affectedStatus === 2) {
indexSampleId = sampleId;
debugDetailed(` Index ID found from affected status in PED: ${indexSampleId}`);
break; // Found the first affected
}
}
if (!indexSampleId) debugDetailed(` No affected ('2') sample found in PED.`);
} else if (!indexSampleId) {
debugDetailed(` Skipping PED check (no PED data).`);
}
// 3. Fallback to first sample in genotype data if still not found
if (!indexSampleId && genotypesMap && genotypesMap.size > 0) {
const firstVariantKey = genotypesMap.keys().next().value;
const firstGenotypes = genotypesMap.get(firstVariantKey);
if (firstGenotypes && firstGenotypes.size > 0) {
indexSampleId = firstGenotypes.keys().next().value;
debugDetailed(` Using fallback index (first sample from first variant): ${indexSampleId}`);
}
}
if (!indexSampleId) {
debug('Could not determine index sample ID for inheritance analysis.');
}
debugDetailed(`--- Final Index Sample ID: ${indexSampleId} ---`);
return indexSampleId;
}
/**
* Groups variant annotations by gene symbol.
* Extracts the gene symbol from the first transcript consequence found,
* with fallback to top-level gene_symbol if available.
* Assigns a placeholder if no gene symbol can be determined.
*
* @param {Array<Object>} annotations - Variant annotation objects with `variantKey`.
* @returns {Map<string, Array<Object>>} Map of gene symbols to their variant annotations.
* @private
*/
function _groupAnnotationsByGene(annotations) {
debugDetailed(`--- Grouping ${annotations?.length || 0} annotations by gene ---`);
const geneVariantsMap = new Map();
if (!annotations || !Array.isArray(annotations)) {
debugDetailed(' No annotations provided for gene grouping.');
return geneVariantsMap;
}
for (const annotation of annotations) {
if (!annotation?.variantKey) {
debugDetailed(' Skipping annotation - missing variantKey');
continue;
}
let geneSymbol = null;
// Find the first gene symbol in the transcript consequences
if (Array.isArray(annotation.transcript_consequences)) {
for (const cons of annotation.transcript_consequences) {
if (cons?.gene_symbol) {
geneSymbol = cons.gene_symbol;
break; // Use the first one found
}
}
}
// Fallback: If no gene symbol in consequences, try top-level annotation
if (!geneSymbol && annotation.gene_symbol) {
geneSymbol = annotation.gene_symbol;
debugDetailed(` Using top-level gene symbol for ${annotation.variantKey}: ${geneSymbol}`);
}
if (!geneSymbol) {
// Use a default placeholder if still no gene symbol
geneSymbol = `NO_GENE_${annotation.seq_region_name || 'UNK'}`;
debugDetailed(
` Variant ${annotation.variantKey} - no gene symbol found. ` +
`Using placeholder: ${geneSymbol}`
);
// Do not skip, group under placeholder
}
// Add to the map
if (!geneVariantsMap.has(geneSymbol)) {
geneVariantsMap.set(geneSymbol, []);
}
geneVariantsMap.get(geneSymbol).push(annotation); // Push the full annotation object
debugDetailed(` Added variant ${annotation.variantKey} to gene group ${geneSymbol}`);
}
debugDetailed(`--- Finished grouping. Found ${geneVariantsMap.size} gene groups. ---`);
return geneVariantsMap;
}
/**
* Merges compound heterozygous results back into the main inheritance results map.
* Updates the prioritized pattern and adds compHet details.
*
* @param {Map<string, Object>} inheritanceResults - Main results map (variantKey -> result).
* @param {string} geneSymbol - The gene being processed.
* @param {Object} compHetResult - The result from analyzeCompoundHeterozygous.
* @private
*/
function _mergeCompHetResults(inheritanceResults, geneSymbol, compHetResult) {
if (!compHetResult || !(compHetResult.isCompHet || compHetResult.isPossible)) {
return; // Nothing to merge
}
// ** Added more detailed logging **
debugDetailed(
` Merging CompHet for ${geneSymbol}:
Confirmed=${compHetResult.isCompHet}, Possible=${compHetResult.isPossible},
Pattern=${compHetResult.pattern}`
);
for (const variantKey of compHetResult.variantKeys) {
if (inheritanceResults.has(variantKey)) {
const currentResult = inheritanceResults.get(variantKey);
debugDetailed(
` Variant ${variantKey}: Current Pattern='${currentResult.prioritizedPattern}'`
);
// Determine if CompHet pattern should override the current pattern
let newPrioritizedPattern = currentResult.prioritizedPattern;
const strongPatterns = [
'de_novo',
'autosomal_recessive',
'x_linked_recessive',
'x_linked_dominant',
'compound_heterozygous', // Confirmed CompHet itself is strong
];
const weakPatterns = [
'unknown', // Covers many unknown_* states
'reference',
'dominant',
'homozygous',
'potential_x_linked',
'non_mendelian',
'autosomal_dominant', // Can be overridden by confirmed CompHet
// Add other potential initial patterns if needed
'autosomal_dominant_possible',
'autosomal_recessive_possible',
'x_linked_recessive_possible',
'x_linked_dominant_possible',
'de_novo_candidate',
// Explicitly add possible CompHet patterns as weak so confirmed can override
'compound_heterozygous_possible',
'compound_heterozygous_possible_missing_parents',
'compound_heterozygous_possible_no_pedigree',
'compound_heterozygous_possible_missing_parent_genotypes',
];
const isCurrentWeak =
weakPatterns.includes(currentResult.prioritizedPattern) ||
currentResult.prioritizedPattern.startsWith('unknown_');
const isCurrentStrong = strongPatterns.includes(currentResult.prioritizedPattern);
debugDetailed(
` Variant ${variantKey}:
isCurrentWeak=${isCurrentWeak}, isCurrentStrong=${isCurrentStrong}`
);
if (compHetResult.isCompHet) {
// Confirmed CompHet overrides weak patterns and autosomal_dominant, but not other strong patterns
if (isCurrentWeak || currentResult.prioritizedPattern === 'autosomal_dominant') {
debugDetailed(
` -> Overriding '${newPrioritizedPattern}' with confirmed 'compound_heterozygous'`
);
newPrioritizedPattern = 'compound_heterozygous';
} else {
debugDetailed(
` -> Keeping strong pattern '${newPrioritizedPattern}' despite confirmed CompHet`
);
}
} else if (compHetResult.isPossible) {
// isPossible = true, isCompHet = false
// Possible CompHet overrides only weak or unknown patterns
if (isCurrentWeak) {
debugDetailed(
` -> Overriding weak pattern '${newPrioritizedPattern}' with possible CompHet ` +
`'${compHetResult.pattern}'`
);
// Use the specific 'possible' pattern from the compHetResult
newPrioritizedPattern = compHetResult.pattern;
} else {
debugDetailed(
` -> Keeping non-weak pattern '${newPrioritizedPattern}' despite possible CompHet`
);
}
}
// Add CompHet details
const compHetDetails = {
isCandidate: compHetResult.isCompHet, // isCandidate true only if confirmed
isPossible: compHetResult.isPossible,
geneSymbol,
// List partners involved in this specific CompHet finding
partnerVariantKeys: compHetResult.variantKeys.filter((k) => k !== variantKey),
// Include PoO info if available from compHetResult
likelyPaternalKeys: compHetResult.paternalVariantKeys, // Keep original keys here
likelyMaternalKeys: compHetResult.maternalVariantKeys,
ambiguousKeys: compHetResult.ambiguousVariantKeys,
};
// Update the result object
const enhancedResult = {
...currentResult,
prioritizedPattern: newPrioritizedPattern, // Use the potentially updated pattern
possiblePatterns: [
...new Set([...(currentResult.possiblePatterns || []), compHetResult.pattern]),
],
// Add/overwrite segregation status for comphet patterns
segregationStatus: {
...currentResult.segregationStatus,
...(compHetResult.isCompHet && { compound_heterozygous: 'segregates' }), // Assume segregates if confirmed
...(compHetResult.isPossible &&
!compHetResult.isCompHet && { [compHetResult.pattern]: 'unknown' }), // Status for possible is unknown
},
compHetDetails: compHetDetails,
};
inheritanceResults.set(variantKey, enhancedResult);
debugDetailed(
` Updated inheritance result for variant ${variantKey} with CompHet info. ` +
`Final Pattern: ${newPrioritizedPattern}`
);
} else {
debugDetailed(
` Warning: Variant key ${variantKey} from CompHet analysis not found in main results.`
);
}
}
}
/**
* Analyzes inheritance patterns for a list of variant annotations.
* This is the main entry point for the inheritance analysis workflow.
*
* @param {Array<Object>} annotations - Variant objects with required properties.
* Each must have a 'variantKey' property in CHR-POS-REF-ALT format.
* @param {Map<string, Map<string, string>>} genotypesMap - Variant genotype maps, keyed by CHR-POS-REF-ALT.
* @param {Map<string, Object>|null} pedigreeData - Optional parsed pedigree data.
* @param {Object|null} sampleMap - Optional role to sample ID mapping.
* @returns {Map<string, Object>} Map of variantKeys (CHR-POS-REF-ALT) to inheritance results.
*/
function analyzeInheritanceForSample(annotations, genotypesMap, pedigreeData, sampleMap) {
debugDetailed(`--- Entering analyzeInheritanceForSample ---`);
debugDetailed(
` Input: ann=${annotations?.length}, gen=${genotypesMap?.size}, ped=${pedigreeData?.size}`
);
// Map: variantKey -> { prioritizedPattern, possiblePatterns, segregationStatus, compHetDetails }
const results = new Map();
if (!Array.isArray(annotations) || !annotations.length) {
debug('No valid annotations for analysis.');
debugDetailed('--- Exiting: No annotations ---');
return results;
}
if (!genotypesMap?.size) {
debug('No genotype data provided for inheritance analysis.');
// Populate results with error state for each annotation key if possible
for (const ann of annotations) {
// Use the variantKey if already present, otherwise try to generate it
const key =
ann.variantKey ||
(() => {
const chrom = ann.seq_region_name || ann.chr || '';
const pos = ann.start || '';
const alleleParts = ann.allele_string ? ann.allele_string.split('/') : [];
const ref = alleleParts[0] || '';
const alt = alleleParts[1] || '';
// Use standardized hyphenated key format here as well
return chrom && pos && ref && alt ? `${chrom}-${pos}-${ref}-${alt}` : null;
})();
if (key) {
ann.variantKey = key; // Ensure variantKey is set
results.set(key, {
prioritizedPattern: 'unknown_missing_genotypes',
possiblePatterns: ['unknown_missing_genotypes'],
segregationStatus: {},
error: 'Genotype map was empty or not provided.',
});
} else {
debugDetailed(
` Cannot generate variantKey for annotation to report missing genotype error:
${JSON.stringify(ann, null, 2)}`
);
}
}
debugDetailed(`--- Exiting analyzeInheritanceForSample: No genotypes ---`);
return results;
}
// Determine the index sample ID (crucial for CompHet analysis)
const indexSampleId = _determineIndexSampleId(genotypesMap, pedigreeData, sampleMap);
// Note: Trio/PED deduction doesn't strictly *need* the index ID identified beforehand,
// but CompHet does.
// --- Pass 1: Initial Pattern Deduction, Segregation, and Prioritization per Variant ---
debugDetailed(`--- Pass 1: Initial Analysis (${annotations.length} variants) ---`);
for (const annotation of annotations) {
// Use the variantKey already assigned in variantLinkerCore
const variantKey = annotation.variantKey;
if (!variantKey) {
debugDetailed(
` SKIPPING - Annotation is missing variantKey. OriginalInput: ${annotation.originalInput || annotation.input}`
);
continue; // Skip processing this annotation if key is missing
}
debugDetailed(
`\nProcessing Annotation:
Key=${variantKey}
Input=${annotation.originalInput || annotation.input}`
);
// *** FIX: Use the correct variantKey (CHR-POS-REF-ALT) for genotype lookup ***
const genotypes = genotypesMap.get(variantKey);
if (!genotypes) {
// This log message is now more accurate if genotypesMap is correct but key is wrong
debugDetailed(` SKIPPING - No genotypes found for key ${variantKey} in genotypesMap.`);
results.set(variantKey, {
prioritizedPattern: 'unknown_missing_genotypes',
possiblePatterns: ['unknown_missing_genotypes'],
segregationStatus: {},
error: `Genotype data not found in map for this variant key ('${variantKey}')`,
});
continue;
}
try {
// 1. Deduce Patterns
// We need chrom info. Extract from key or annotation if possible.
const keyParts = variantKey.split('-'); // Use hyphen now
const chrom = keyParts[0] || annotation.seq_region_name || annotation.chr || 'unknown';
const variantInfo = { chrom: chrom }; // Use consistent chrom value
debugDetailed(` --> Calling patternDeducer.deduceInheritancePatterns for ${variantKey}...`);
const possiblePatterns = patternDeducer.deduceInheritancePatterns(
genotypes,
pedigreeData,
sampleMap,
variantInfo
);
debugDetailed(` <-- deduceInheritancePatterns result: ${JSON.stringify(possiblePatterns)}`);
// 2. Check Segregation (if pedigree data available)
let segregationResults = null;
if (pedigreeData && pedigreeData.size > 0 && possiblePatterns.length > 0) {
debugDetailed(` --> Checking segregation for ${possiblePatterns.length} patterns...`);
segregationResults = new Map();
for (const pattern of possiblePatterns) {
// ** Refined list of patterns to skip segregation check for **
const patternsToSkipSegCheck = [
'unknown',
'reference',
'dominant',
'homozygous',
'potential_x_linked',
'non_mendelian',
'autosomal_dominant_possible',
'autosomal_recessive_possible',
'x_linked_recessive_possible',
'x_linked_dominant_possible',
'compound_heterozygous_possible', // Also skip possible CompHet patterns
'de_novo_candidate',
];
// Check if pattern itself or starts with a skippable prefix (e.g., unknown_, error_)
const shouldSkip =
patternsToSkipSegCheck.includes(pattern) ||
pattern.startsWith('error_') ||
pattern.startsWith('unknown_') ||
pattern.startsWith('compound_heterozygous_possible'); // Catch all possible CompHet variants
if (shouldSkip) {
debugDetailed(` Skipping segregation check for non-definitive pattern: ${pattern}`);
continue;
}
try {
debugDetailed(
` ---> Calling segregationChecker.checkSegregation for pattern: ${pattern}...`
);
const status = segregationChecker.checkSegregation(pattern, genotypes, pedigreeData);
segregationResults.set(pattern, status);
debugDetailed(` <--- checkSegregation result: ${status}`);
} catch (segError) {
debugDetailed(` !!! ERROR checking segregation for ${pattern}: ${segError.message}`);
segregationResults.set(pattern, 'error_checking_segregation');
}
}
debugDetailed(
` <-- Segregation checks complete. Results map size: ${segregationResults.size}`
);
} else {
debugDetailed(` Skipping segregation check (No PED data or no patterns).`);
}
// 3. Prioritize Pattern
debugDetailed(` --> Calling patternPrioritizer.prioritizePattern...`);
const prioritizedPattern = patternPrioritizer.prioritizePattern(
possiblePatterns,
segregationResults
);
debugDetailed(` <-- prioritizePattern result: ${prioritizedPattern}`);
// Store initial result using the correct variantKey
results.set(variantKey, {
prioritizedPattern,
possiblePatterns,
segregationStatus: segregationResults ? Object.fromEntries(segregationResults) : {},
// compHetDetails will be added in Pass 2 if applicable
});
debugDetailed(` Stored initial inheritance result for key ${variantKey}.`);
} catch (error) {
debug(`!!! ERROR analyzing inheritance for variant ${variantKey}: ${error.message} !!!`);
debugDetailed(`Stack trace: ${error.stack}`);
// Store error using the correct variantKey
results.set(variantKey, {
prioritizedPattern: 'error_analysis_failed',
possiblePatterns: ['error_analysis_failed'],
segregationStatus: {},
error: `Analysis failed: ${error.message}`,
});
}
} // End Pass 1 loop
// --- Pass 2: Compound Heterozygous Analysis ---
debugDetailed(`--- Starting Pass 2: Compound Heterozygous Analysis ---`);
if (!indexSampleId) {
debug(
'Skipping Compound Heterozygous analysis because Index Sample ID could not be determined.'
);
debugDetailed(`--- Exiting analyzeInheritanceForSample (Skipped CompHet) ---`);
return results; // Return results from Pass 1
}
const geneVariantsMap = _groupAnnotationsByGene(annotations);
if (geneVariantsMap.size === 0) {
debugDetailed('No gene groups found for compound heterozygous analysis.');
} else {
debugDetailed(`Analyzing ${geneVariantsMap.size} gene groups for compound heterozygosity...`);
for (const [geneSymbol, geneVariants] of geneVariantsMap.entries()) {
// ** Skip placeholder gene groups **
if (geneSymbol.startsWith('NO_GENE_')) {
debugDetailed(` Skipping placeholder gene group ${geneSymbol} for CompHet.`);
continue;
}
if (geneVariants.length < 2) {
debugDetailed(` Skipping gene ${geneSymbol} - only ${geneVariants.length} variant(s).`);
continue;
}
// Only attempt CompHet analysis if pedigree data is available (needed for confirmation)
// The analyzer handles missing PED internally now, but log remains useful.
if (!pedigreeData || pedigreeData.size === 0) {
debugDetailed(
` Proceeding with CompHet analysis for ${geneSymbol}
without PED data (will yield 'possible_no_pedigree').`
);
}
debugDetailed(` Analyzing gene ${geneSymbol} (${geneVariants.length} variants)...`);
try {
// Ensure compoundHetAnalyzer also receives the correct genotypesMap keyed by CHR-POS-REF-ALT
const compHetResult = compoundHetAnalyzer.analyzeCompoundHeterozygous(
geneVariants,
genotypesMap, // Pass the map keyed by CHR-POS-REF-ALT
pedigreeData, // Pass PED data, analyzer handles null/missing case
indexSampleId // Crucial parameter
);
if (compHetResult) {
debugDetailed(` --> Merging CompHet results for ${geneSymbol}...`);
// Ensure _mergeCompHetResults uses the correct hyphenated keys
_mergeCompHetResults(results, geneSymbol, compHetResult);
debugDetailed(` <-- Finished merging CompHet results for ${geneSymbol}.`);
} else {
debugDetailed(` No applicable CompHet pattern found for ${geneSymbol}.`);
}
} catch (compHetError) {
debug(`!!! CompHet error for ${geneSymbol}: ${compHetError.message} !!!`);
debugDetailed(`Stack trace: ${compHetError.stack}`);
// Optionally mark involved variants with an error status
for (const variant of geneVariants) {
// Use the correct key to update the results map
if (results.has(variant.variantKey)) {
const currentResult = results.get(variant.variantKey);
results.set(variant.variantKey, {
...currentResult,
compHetDetails: { error: `CompHet analysis failed: ${compHetError.message}` },
});
}
}
}
}
} // End Pass 2
debugDetailed(`--- Completed inheritance analysis. Final results map size: ${results.size} ---`);
return results;
}
// --- Exports ---
module.exports = {
// Core function
analyzeInheritanceForSample,
// Do not export internal helper functions
// (_determineIndexSampleId, _groupAnnotationsByGene, _mergeCompHetResults)
};