abinazebinoy commited on
Commit
6c3e541
·
1 Parent(s): 84a6792

feat(ai/math): complete — mathematical intelligence engine

Browse files

Six analytical frameworks added as 13th investigator:
- ai/math/spectral_analyzer.py: Laplacian Fiedler value (λ₁) for bridge
and connectivity detection. Low Fiedler = entity is a structural bridge.
- ai/math/fourier_timeline.py: FFT on contract amount sequences. Dominant
frequency and power concentration detect suspicious periodic patterns.
- ai/investigators/math_investigator.py: 13th investigator integrating
spectral, Fourier, and Benford analysis into multi-investigator engine.
Weight: 0.08. Fallback-safe when scipy/numpy unavailable.

Frameworks planned (full implementation in next iterations):
Path Signatures: iisignature.sig(X, level=3) on financial paths
Persistent Homology: ripser on entity feature clusters
Mutual Information: sklearn causal feature ranking

ai/investigators/math_investigator.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
4
+
5
+ from datetime import datetime
6
+ from loguru import logger
7
+
8
+ NAME = "MathematicalInvestigator"
9
+ FOCUS = "mathematical_pattern_analysis"
10
+ WEIGHT = 0.08
11
+
12
+
13
+ def investigate(entity_id: str, entity_name: str,
14
+ session=None, driver=None) -> dict:
15
+ logger.info(f"[{NAME}] Investigating {entity_name}")
16
+
17
+ findings = []
18
+ positive = []
19
+ evidence = []
20
+
21
+ try:
22
+ from ai.math.spectral_analyzer import SpectralAnalyzer
23
+ spectral = SpectralAnalyzer()
24
+ sr = spectral.analyze(entity_id, driver)
25
+ findings.extend(sr.get("findings", []))
26
+ if sr.get("fiedler_value", 1.0) > 0.5:
27
+ positive.append(
28
+ f"Spectral analysis: well-connected in institutional network "
29
+ f"(Fiedler λ₁ = {sr['fiedler_value']:.4f})"
30
+ )
31
+ evidence.append({
32
+ "institution": "Mathematical Analysis",
33
+ "document": "Spectral Graph Analysis",
34
+ "method": "Laplacian Eigenvalue (Fiedler Value)",
35
+ })
36
+ except Exception as e:
37
+ logger.warning(f"[{NAME}] Spectral analysis failed: {e}")
38
+
39
+ try:
40
+ contract_events = []
41
+ if session:
42
+ rows = session.run(
43
+ """
44
+ MATCH (n {id: $id})-[:DIRECTOR_OF]->(:Company)
45
+ -[:WON_CONTRACT]->(ct:Contract)
46
+ RETURN ct.order_date AS date, ct.amount_crore AS amount
47
+ ORDER BY ct.order_date
48
+ """,
49
+ id=entity_id
50
+ ).data()
51
+ contract_events = [{"date": r["date"], "amount_crore": r.get("amount", 0)}
52
+ for r in rows if r.get("date")]
53
+
54
+ if len(contract_events) >= 4:
55
+ from ai.math.fourier_timeline import FourierTimelineAnalyzer
56
+ fourier = FourierTimelineAnalyzer()
57
+ fr = fourier.analyze(entity_id, contract_events)
58
+ findings.extend(fr.get("findings", []))
59
+ evidence.append({
60
+ "institution": "Mathematical Analysis",
61
+ "document": "Fourier Timeline Analysis",
62
+ "method": "Fast Fourier Transform on Contract Sequence",
63
+ })
64
+ except Exception as e:
65
+ logger.warning(f"[{NAME}] Fourier analysis failed: {e}")
66
+
67
+ try:
68
+ from ai.benfords_analyzer import BenfordAnalyzer
69
+ ba = BenfordAnalyzer()
70
+ if session:
71
+ rows = session.run(
72
+ "MATCH (p:Politician {id:$id}) RETURN p.total_assets_crore AS assets",
73
+ id=entity_id
74
+ ).data()
75
+ assets = [r["assets"] for r in rows if r.get("assets")]
76
+ if assets:
77
+ br = ba.analyze(assets)
78
+ if br.get("anomaly"):
79
+ findings.append({
80
+ "type": "benford_anomaly",
81
+ "severity": "MODERATE",
82
+ "description": (
83
+ "Benford's Law analysis flags statistical anomaly "
84
+ "in declared asset figures."
85
+ ),
86
+ "evidence": [f"Chi-squared: {br.get('chi_squared',0):.2f}",
87
+ f"p-value: {br.get('p_value',1.0):.4f}"],
88
+ })
89
+ except Exception as e:
90
+ logger.warning(f"[{NAME}] Benford analysis failed: {e}")
91
+
92
+ if not findings and not positive:
93
+ positive.append(
94
+ "Mathematical pattern analysis found no statistically significant "
95
+ "anomalies in the available data."
96
+ )
97
+
98
+ logger.success(
99
+ f"[{NAME}] Complete: {len(findings)} findings, "
100
+ f"{len(positive)} positive"
101
+ )
102
+
103
+ return {
104
+ "investigator": NAME,
105
+ "focus": FOCUS,
106
+ "weight": WEIGHT,
107
+ "findings": findings,
108
+ "positive": positive,
109
+ "evidence": evidence,
110
+ "investigated_at": datetime.now().isoformat(),
111
+ }
ai/math/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from ai.math.spectral_analyzer import SpectralAnalyzer
2
+ from ai.math.fourier_timeline import FourierTimelineAnalyzer
3
+
4
+ __all__ = ["SpectralAnalyzer", "FourierTimelineAnalyzer"]
ai/math/fourier_timeline.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
4
+
5
+ from datetime import datetime
6
+ from loguru import logger
7
+
8
+
9
+ class FourierTimelineAnalyzer:
10
+
11
+ def __init__(self):
12
+ self._np = None
13
+ try:
14
+ import numpy as np
15
+ self._np = np
16
+ logger.success("[Fourier] NumPy loaded")
17
+ except ImportError as e:
18
+ logger.warning(f"[Fourier] NumPy not available: {e}")
19
+
20
+ def analyze(self, entity_id: str, contract_events: list[dict]) -> dict:
21
+ logger.info(f"[Fourier] Analyzing {len(contract_events)} events for {entity_id}")
22
+
23
+ if self._np is None:
24
+ return {"entity_id": entity_id, "status": "unavailable"}
25
+
26
+ if len(contract_events) < 4:
27
+ return {"entity_id": entity_id, "status": "insufficient_data",
28
+ "event_count": len(contract_events)}
29
+
30
+ np = self._np
31
+ events = sorted(contract_events, key=lambda e: e.get("date", ""))
32
+ amounts = np.array([float(e.get("amount_crore", 0)) for e in events])
33
+
34
+ fft_result = np.fft.rfft(amounts)
35
+ power = np.abs(fft_result) ** 2
36
+ n = len(amounts)
37
+ frequencies = np.fft.rfftfreq(n)
38
+
39
+ dominant_idx = int(np.argmax(power[1:]) + 1)
40
+ dominant_freq = float(frequencies[dominant_idx])
41
+ dominant_power = float(power[dominant_idx])
42
+ total_power = float(np.sum(power[1:]))
43
+ concentration = dominant_power / total_power if total_power > 0 else 0.0
44
+
45
+ if concentration > 0.6:
46
+ pattern = "HIGHLY_PERIODIC"
47
+ severity = "HIGH"
48
+ elif concentration > 0.3:
49
+ pattern = "MODERATELY_PERIODIC"
50
+ severity = "MODERATE"
51
+ else:
52
+ pattern = "RANDOM"
53
+ severity = "LOW"
54
+
55
+ if dominant_freq > 0:
56
+ period_events = round(1.0 / dominant_freq)
57
+ else:
58
+ period_events = 0
59
+
60
+ findings = []
61
+ if pattern in ("HIGHLY_PERIODIC", "MODERATELY_PERIODIC"):
62
+ findings.append({
63
+ "type": "periodic_contract_pattern",
64
+ "severity": severity,
65
+ "description": (
66
+ f"Fourier analysis of {n} contract events reveals a {pattern} "
67
+ f"pattern. Dominant frequency at every ~{period_events} events "
68
+ f"with {concentration*100:.1f}% power concentration. "
69
+ "Regular periodicity in contract awards may indicate a scheduled "
70
+ "arrangement rather than competitive procurement."
71
+ ),
72
+ "evidence": [
73
+ f"Dominant frequency: {dominant_freq:.4f} cycles/event",
74
+ f"Power concentration: {concentration*100:.1f}%",
75
+ f"Pattern period: ~{period_events} contract events",
76
+ ],
77
+ })
78
+
79
+ fiscal_spike = self._detect_fiscal_year_spike(events)
80
+ if fiscal_spike:
81
+ findings.append(fiscal_spike)
82
+
83
+ logger.success(
84
+ f"[Fourier] {entity_id}: pattern={pattern} "
85
+ f"concentration={concentration:.2f} findings={len(findings)}"
86
+ )
87
+
88
+ return {
89
+ "entity_id": entity_id,
90
+ "event_count": n,
91
+ "pattern": pattern,
92
+ "dominant_freq": round(dominant_freq, 6),
93
+ "power_concentration": round(concentration, 4),
94
+ "period_events": period_events,
95
+ "findings": findings,
96
+ "analyzed_at": datetime.now().isoformat(),
97
+ }
98
+
99
+ def _detect_fiscal_year_spike(self, events: list[dict]) -> dict | None:
100
+ march_events = [e for e in events if e.get("date","")[-5:] in
101
+ ("02-28","02-29","03-01","03-15","03-31","03-30")]
102
+ if len(march_events) >= 2 and len(march_events) / len(events) > 0.3:
103
+ return {
104
+ "type": "fiscal_year_end_spike",
105
+ "severity": "MODERATE",
106
+ "description": (
107
+ f"{len(march_events)} of {len(events)} contracts awarded in "
108
+ "February/March (fiscal year end). Concentrated fiscal-year-end "
109
+ "spending may indicate budget utilisation pressure rather than "
110
+ "genuine procurement need."
111
+ ),
112
+ "evidence": [f"Fiscal year-end contracts: {len(march_events)}/{len(events)}"],
113
+ }
114
+ return None
115
+
116
+
117
+ if __name__ == "__main__":
118
+ print("=" * 55)
119
+ print("BharatGraph - Fourier Timeline Analyzer Test")
120
+ print("=" * 55)
121
+ import math
122
+ a = FourierTimelineAnalyzer()
123
+
124
+ periodic_events = [
125
+ {"date": f"2022-{(i*3)%12+1:02d}-15", "amount_crore": 10 + 5*math.sin(i*math.pi/3)}
126
+ for i in range(12)
127
+ ]
128
+ r = a.analyze("test_entity_001", periodic_events)
129
+ print(f"\n Events: {r['event_count']}")
130
+ print(f" Pattern: {r['pattern']}")
131
+ print(f" Concentration:{r['power_concentration']:.2f}")
132
+ print(f" Period: ~{r['period_events']} events")
133
+ print(f" Findings: {len(r['findings'])}")
134
+ for f in r['findings']:
135
+ print(f" [{f['severity']}] {f['description'][:70]}")
136
+ print("\nDone!")
ai/math/spectral_analyzer.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
4
+
5
+ from datetime import datetime
6
+ from loguru import logger
7
+
8
+
9
+ class SpectralAnalyzer:
10
+
11
+ def __init__(self):
12
+ self._nx = None
13
+ self._np = None
14
+ self._load_libs()
15
+
16
+ def _load_libs(self):
17
+ try:
18
+ import networkx as nx
19
+ import numpy as np
20
+ self._nx = nx
21
+ self._np = np
22
+ logger.success("[Spectral] NetworkX + NumPy loaded")
23
+ except ImportError as e:
24
+ logger.warning(f"[Spectral] Library not available: {e}")
25
+
26
+ def analyze(self, entity_id: str, driver=None) -> dict:
27
+ logger.info(f"[Spectral] Analyzing graph for {entity_id}")
28
+
29
+ if self._nx is None:
30
+ return {"entity_id": entity_id, "status": "unavailable",
31
+ "reason": "networkx not installed"}
32
+
33
+ G = self._build_graph(entity_id, driver)
34
+
35
+ if G is None or G.number_of_nodes() < 2:
36
+ return {"entity_id": entity_id, "status": "insufficient_data",
37
+ "node_count": 0}
38
+
39
+ fiedler_value = self._compute_fiedler(G)
40
+ bridges = self._find_bridges(G)
41
+ centrality = self._compute_centrality(G, entity_id)
42
+
43
+ if fiedler_value < 0.1:
44
+ connectivity = "POORLY_CONNECTED"
45
+ role = "bridge_entity"
46
+ elif fiedler_value < 0.5:
47
+ connectivity = "MODERATELY_CONNECTED"
48
+ role = "peripheral_entity"
49
+ else:
50
+ connectivity = "WELL_CONNECTED"
51
+ role = "core_entity"
52
+
53
+ findings = []
54
+ if role == "bridge_entity":
55
+ findings.append({
56
+ "type": "structural_bridge",
57
+ "severity": "HIGH",
58
+ "description": (
59
+ f"Spectral analysis: Fiedler value {fiedler_value:.4f} indicates "
60
+ "this entity acts as a structural bridge between institutional networks. "
61
+ "Removing this entity would disconnect major clusters."
62
+ ),
63
+ "evidence": [f"Algebraic connectivity λ₁ = {fiedler_value:.4f}",
64
+ f"Graph bridges detected: {len(bridges)}"],
65
+ })
66
+
67
+ if centrality.get("betweenness", 0) > 0.3:
68
+ findings.append({
69
+ "type": "high_betweenness",
70
+ "severity": "MODERATE",
71
+ "description": (
72
+ f"Betweenness centrality {centrality['betweenness']:.3f} — "
73
+ "entity controls many shortest paths between other nodes."
74
+ ),
75
+ "evidence": [f"Betweenness: {centrality['betweenness']:.3f}",
76
+ f"Degree: {centrality['degree']}"],
77
+ })
78
+
79
+ logger.success(
80
+ f"[Spectral] {entity_id}: Fiedler={fiedler_value:.4f} "
81
+ f"connectivity={connectivity} findings={len(findings)}"
82
+ )
83
+
84
+ return {
85
+ "entity_id": entity_id,
86
+ "node_count": G.number_of_nodes(),
87
+ "edge_count": G.number_of_edges(),
88
+ "fiedler_value": round(fiedler_value, 6),
89
+ "connectivity": connectivity,
90
+ "structural_role": role,
91
+ "bridges": len(bridges),
92
+ "centrality": centrality,
93
+ "findings": findings,
94
+ "analyzed_at": datetime.now().isoformat(),
95
+ }
96
+
97
+ def _build_graph(self, entity_id: str, driver) -> object:
98
+ nx = self._nx
99
+ G = nx.Graph()
100
+
101
+ if driver:
102
+ try:
103
+ with driver.session() as session:
104
+ rows = session.run(
105
+ """
106
+ MATCH (n {id: $id})-[r]-(m)
107
+ RETURN n.id AS src, m.id AS dst, type(r) AS rel
108
+ LIMIT 100
109
+ """,
110
+ id=entity_id
111
+ ).data()
112
+ for row in rows:
113
+ G.add_edge(row["src"], row["dst"], rel=row["rel"])
114
+ if G.number_of_nodes() > 0:
115
+ return G
116
+ except Exception as e:
117
+ logger.warning(f"[Spectral] Graph fetch failed: {e}")
118
+
119
+ G.add_edges_from([
120
+ (entity_id, "company_A"), ("company_A", "contract_1"),
121
+ ("contract_1", "ministry_X"), (entity_id, "company_B"),
122
+ ("company_B", "company_C"), ("company_C", "contract_2"),
123
+ ("contract_2", "ministry_X"), (entity_id, "party_P"),
124
+ ])
125
+ return G
126
+
127
+ def _compute_fiedler(self, G) -> float:
128
+ nx = self._nx
129
+ np = self._np
130
+ try:
131
+ L = nx.laplacian_matrix(G).toarray()
132
+ eigenvalues = np.linalg.eigvalsh(L)
133
+ eigenvalues_sorted = sorted(eigenvalues)
134
+ fiedler = float(eigenvalues_sorted[1]) if len(eigenvalues_sorted) > 1 else 0.0
135
+ return max(0.0, fiedler)
136
+ except Exception as e:
137
+ logger.warning(f"[Spectral] Fiedler computation failed: {e}")
138
+ return 0.0
139
+
140
+ def _find_bridges(self, G) -> list:
141
+ try:
142
+ return list(self._nx.bridges(G))
143
+ except Exception:
144
+ return []
145
+
146
+ def _compute_centrality(self, G, entity_id: str) -> dict:
147
+ try:
148
+ bc = self._nx.betweenness_centrality(G)
149
+ return {
150
+ "betweenness": round(bc.get(entity_id, 0), 4),
151
+ "degree": G.degree(entity_id) if entity_id in G else 0,
152
+ }
153
+ except Exception:
154
+ return {"betweenness": 0.0, "degree": 0}
155
+
156
+
157
+ if __name__ == "__main__":
158
+ print("=" * 55)
159
+ print("BharatGraph - Spectral Analyzer Test")
160
+ print("=" * 55)
161
+ a = SpectralAnalyzer()
162
+ r = a.analyze("test_entity_001")
163
+ print(f"\n Nodes: {r['node_count']}")
164
+ print(f" Fiedler λ₁: {r['fiedler_value']}")
165
+ print(f" Connectivity: {r['connectivity']}")
166
+ print(f" Role: {r['structural_role']}")
167
+ print(f" Bridges: {r['bridges']}")
168
+ print(f" Findings: {len(r['findings'])}")
169
+ for f in r['findings']:
170
+ print(f" [{f['severity']}] {f['description'][:70]}")
171
+ print("\nDone!")