File size: 5,241 Bytes
3d015cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Fusion Engine - Confidence-weighted Score Fusion"""
from typing import Dict, Tuple, Optional
from config import Config

class FusionEngine:
    """Combines scores from all modules with confidence weighting"""
    
    def __init__(self):
        # Base weights (when no domain evidence)
        self.base_weights = {
            'universal': Config.UNIVERSAL_WEIGHT,
            'personality': Config.PERSONALITY_WEIGHT,
            'text': Config.TEXT_WEIGHT
        }
        
        # Extended weights (when domain evidence exists)
        self.extended_weights = {
            'universal': 0.30,  # Reduced from base
            'personality': 0.25,
            'text': 0.25,
            'domain': 0.20  # New domain component
        }
    
    def fuse_scores(
        self,
        universal_score: float,
        universal_confidence: float,
        personality_score: float,
        personality_confidence: float,
        text_score: float,
        text_confidence: float,
        domain_score: Optional[float] = None,
        domain_confidence: Optional[float] = None
    ) -> Tuple[float, Dict]:
        """
        Fuse scores with confidence weighting
        Supports optional domain score for pluggable domain evidence
        
        Returns: (final_score, breakdown)
        """
        
        # Determine which weights to use
        has_domain = domain_score is not None and domain_confidence is not None and domain_confidence > 0
        weights = self.extended_weights if has_domain else self.base_weights
        
        # Calculate effective weights (weight * confidence)
        effective_weights = {
            'universal': weights['universal'] * universal_confidence,
            'personality': weights['personality'] * personality_confidence,
            'text': weights['text'] * text_confidence
        }
        
        # Add domain if available
        if has_domain:
            effective_weights['domain'] = weights['domain'] * domain_confidence
        
        # Sum of effective weights (for normalization)
        total_effective_weight = sum(effective_weights.values())
        
        # Prevent division by zero
        if total_effective_weight == 0:
            breakdown = {
                'final_score': 0.0,
                'component_scores': {
                    'universal': 0.0,
                    'personality': 0.0,
                    'text': 0.0
                },
                'confidences': {
                    'universal': 0.0,
                    'personality': 0.0,
                    'text': 0.0
                },
                'effective_weights': effective_weights,
                'has_domain': False
            }
            
            if has_domain:
                breakdown['component_scores']['domain'] = 0.0
                breakdown['confidences']['domain'] = 0.0
            
            return 0.0, breakdown
        
        # Calculate fused score
        fused_score = (
            effective_weights['universal'] * universal_score +
            effective_weights['personality'] * personality_score +
            effective_weights['text'] * text_score
        )
        
        if has_domain:
            fused_score += effective_weights['domain'] * domain_score
        
        fused_score /= total_effective_weight
        
        # Prepare breakdown
        breakdown = {
            'final_score': round(fused_score, 4),
            'component_scores': {
                'universal': round(universal_score, 4),
                'personality': round(personality_score, 4),
                'text': round(text_score, 4)
            },
            'confidences': {
                'universal': round(universal_confidence, 4),
                'personality': round(personality_confidence, 4),
                'text': round(text_confidence, 4)
            },
            'effective_weights': {
                k: round(v / total_effective_weight, 4)
                for k, v in effective_weights.items()
            },
            'base_weights': weights,
            'has_domain': has_domain
        }
        
        # Add domain info if present
        if has_domain:
            breakdown['component_scores']['domain'] = round(domain_score, 4)
            breakdown['confidences']['domain'] = round(domain_confidence, 4)
        
        return fused_score, breakdown
    
    def get_grade(self, final_score: float) -> str:
        """Convert score to letter grade"""
        if final_score >= 0.9:
            return 'A+'
        elif final_score >= 0.85:
            return 'A'
        elif final_score >= 0.8:
            return 'A-'
        elif final_score >= 0.75:
            return 'B+'
        elif final_score >= 0.7:
            return 'B'
        elif final_score >= 0.65:
            return 'B-'
        elif final_score >= 0.6:
            return 'C+'
        elif final_score >= 0.55:
            return 'C'
        elif final_score >= 0.5:
            return 'C-'
        else:
            return 'D'
    
    def get_percentile(self, final_score: float) -> int:
        """Estimate percentile (mock for MVP)"""
        # In production, this would query actual distribution
        return min(int(final_score * 100), 99)