entroplain 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,390 +1,390 @@
1
- """
2
- Entropy Monitor — Core entropy tracking and early exit logic.
3
-
4
- Supports multiple exit strategies:
5
- - Valleys plateau: Exit when reasoning milestones stabilize
6
- - Entropy drop: Exit when model confidence is high
7
- - Velocity zero: Exit when entropy stops changing
8
- - Combined: Multiple conditions with AND/OR logic
9
- - Repetition: Exit when model starts repeating
10
- - Confidence: Exit when top token probability > threshold for N tokens
11
- """
12
-
13
- import math
14
- from typing import List, Tuple, Optional, Dict, Any, Callable
15
- from dataclasses import dataclass, field
16
- from enum import Enum
17
- from collections import Counter
18
-
19
-
20
- class ExitCondition(Enum):
21
- VALLEYS_PLATEAU = "valleys_plateau"
22
- ENTROPY_DROP = "entropy_drop"
23
- VELOCITY_ZERO = "velocity_zero"
24
- COMBINED = "combined"
25
- # New strategies
26
- REPETITION = "repetition"
27
- CONFIDENCE = "confidence"
28
- SEMANTIC = "semantic"
29
-
30
-
31
- @dataclass
32
- class EntropyPoint:
33
- """A single point in the entropy trajectory."""
34
- index: int
35
- token: str
36
- entropy: float
37
- is_valley: bool = False
38
- velocity: float = 0.0
39
- confidence: float = 0.0 # Top token probability
40
-
41
-
42
- @dataclass
43
- class MonitorConfig:
44
- """Configuration for the entropy monitor."""
45
- entropy_threshold: float = 0.15
46
- min_valleys: int = 2
47
- velocity_threshold: float = 0.05
48
- min_tokens: int = 50
49
- valley_window: int = 5
50
- plateau_threshold: int = 3
51
- exit_condition: ExitCondition = ExitCondition.COMBINED
52
- # New config options
53
- repetition_window: int = 20 # Window to check for repetition
54
- repetition_threshold: float = 0.3 # 30% repetition = exit
55
- confidence_threshold: float = 0.95 # 95% confidence = exit
56
- confidence_min_tokens: int = 5 # Min tokens at high confidence
57
-
58
-
59
- class EntropyMonitor:
60
- """
61
- Monitor entropy trajectory and detect reasoning convergence.
62
-
63
- Usage:
64
- monitor = EntropyMonitor()
65
- for token, entropy in stream:
66
- monitor.track(token, entropy)
67
- if monitor.should_exit():
68
- break
69
- """
70
-
71
- def __init__(
72
- self,
73
- entropy_threshold: float = 0.15,
74
- min_valleys: int = 2,
75
- velocity_threshold: float = 0.05,
76
- min_tokens: int = 50,
77
- valley_window: int = 5,
78
- plateau_threshold: int = 3,
79
- exit_condition: str = "combined",
80
- # New parameters
81
- repetition_window: int = 20,
82
- repetition_threshold: float = 0.3,
83
- confidence_threshold: float = 0.95,
84
- confidence_min_tokens: int = 5,
85
- ):
86
- self.config = MonitorConfig(
87
- entropy_threshold=entropy_threshold,
88
- min_valleys=min_valleys,
89
- velocity_threshold=velocity_threshold,
90
- min_tokens=min_tokens,
91
- valley_window=valley_window,
92
- plateau_threshold=plateau_threshold,
93
- exit_condition=ExitCondition(exit_condition),
94
- repetition_window=repetition_window,
95
- repetition_threshold=repetition_threshold,
96
- confidence_threshold=confidence_threshold,
97
- confidence_min_tokens=confidence_min_tokens,
98
- )
99
- self._trajectory: List[EntropyPoint] = []
100
- self._valleys: List[EntropyPoint] = []
101
- self._index = 0
102
- self._high_confidence_count = 0 # Track consecutive high confidence
103
-
104
- def calculate_entropy(self, logprobs: List[float], from_probs: bool = False) -> float:
105
- """
106
- Calculate Shannon entropy from log probabilities or probabilities.
107
-
108
- Args:
109
- logprobs: List of log probabilities (natural log) or probabilities
110
- from_probs: If True, treat input as probabilities (will convert)
111
-
112
- Returns:
113
- Shannon entropy in bits
114
- """
115
- if not logprobs:
116
- return 0.0
117
-
118
- entropy = 0.0
119
- for lp in logprobs:
120
- if from_probs:
121
- prob = lp
122
- else:
123
- prob = math.exp(lp)
124
- if prob > 0:
125
- entropy -= prob * math.log2(prob + 1e-10)
126
-
127
- return entropy
128
-
129
- def track(self, token: str, entropy: float, confidence: float = 0.0) -> EntropyPoint:
130
- """
131
- Track a token and its entropy value.
132
-
133
- Args:
134
- token: The generated token
135
- entropy: Calculated entropy for this token
136
- confidence: Top token probability (optional, for confidence strategy)
137
-
138
- Returns:
139
- EntropyPoint with valley detection
140
- """
141
- point = EntropyPoint(
142
- index=self._index,
143
- token=token,
144
- entropy=entropy,
145
- confidence=confidence
146
- )
147
-
148
- # Calculate velocity
149
- if len(self._trajectory) > 0:
150
- prev = self._trajectory[-1]
151
- point.velocity = abs(entropy - prev.entropy)
152
-
153
- # Detect valley (local minimum)
154
- if len(self._trajectory) >= 2:
155
- prev2 = self._trajectory[-2]
156
- prev1 = self._trajectory[-1]
157
- if prev1.entropy < prev2.entropy and prev1.entropy < entropy:
158
- prev1.is_valley = True
159
- self._valleys.append(prev1)
160
-
161
- self._trajectory.append(point)
162
- self._index += 1
163
-
164
- # Track high confidence
165
- if confidence >= self.config.confidence_threshold:
166
- self._high_confidence_count += 1
167
- else:
168
- self._high_confidence_count = 0
169
-
170
- return point
171
-
172
- def get_valleys(self) -> List[Tuple[int, float]]:
173
- """Get all entropy valleys (local minima) as (index, entropy) tuples."""
174
- return [(v.index, v.entropy) for v in self._valleys]
175
-
176
- def get_velocity(self) -> float:
177
- """Get current entropy velocity (rate of change)."""
178
- if len(self._trajectory) < 2:
179
- return 0.0
180
- return self._trajectory[-1].velocity
181
-
182
- def get_mean_entropy(self) -> float:
183
- """Get mean entropy over the trajectory."""
184
- if not self._trajectory:
185
- return 0.0
186
- return sum(p.entropy for p in self._trajectory) / len(self._trajectory)
187
-
188
- def get_valley_count(self) -> int:
189
- """Get the number of detected valleys."""
190
- return len(self._valleys)
191
-
192
- def is_valleys_plateau(self) -> bool:
193
- """Check if valley count has plateaued."""
194
- if len(self._valleys) < self.config.min_valleys:
195
- return False
196
-
197
- # Check if last N valleys have similar spacing
198
- recent = self._valleys[-self.config.plateau_threshold:]
199
- if len(recent) < self.config.plateau_threshold:
200
- return False
201
-
202
- # Calculate spacing between recent valleys
203
- spacings = [
204
- recent[i + 1].index - recent[i].index
205
- for i in range(len(recent) - 1)
206
- ]
207
- if not spacings:
208
- return False
209
-
210
- mean_spacing = sum(spacings) / len(spacings)
211
- variance = sum((s - mean_spacing) ** 2 for s in spacings) / len(spacings)
212
-
213
- # Low variance in spacing = plateau
214
- return variance < 10 # Threshold tuned empirically
215
-
216
- def is_entropy_low(self) -> bool:
217
- """Check if current entropy is below threshold."""
218
- if not self._trajectory:
219
- return False
220
- return self._trajectory[-1].entropy < self.config.entropy_threshold
221
-
222
- def is_velocity_stable(self) -> bool:
223
- """Check if velocity is below threshold."""
224
- return self.get_velocity() < self.config.velocity_threshold
225
-
226
- def is_repeating(self) -> bool:
227
- """
228
- Check if the model is repeating itself.
229
-
230
- Returns True if the repetition ratio in the recent window
231
- exceeds the threshold.
232
- """
233
- if len(self._trajectory) < self.config.repetition_window:
234
- return False
235
-
236
- # Get recent tokens
237
- recent_tokens = [
238
- p.token for p in self._trajectory[-self.config.repetition_window :]
239
- ]
240
-
241
- # Count unique vs total
242
- counter = Counter(recent_tokens)
243
- unique_count = len(counter)
244
- total_count = len(recent_tokens)
245
-
246
- # Calculate repetition ratio
247
- repetition_ratio = 1.0 - (unique_count / total_count)
248
-
249
- return repetition_ratio >= self.config.repetition_threshold
250
-
251
- def is_confident(self) -> bool:
252
- """
253
- Check if model has been highly confident for consecutive tokens.
254
-
255
- Returns True if the last N tokens had confidence >= threshold.
256
- """
257
- return self._high_confidence_count >= self.config.confidence_min_tokens
258
-
259
- def should_exit(self) -> bool:
260
- """
261
- Determine if reasoning has converged and we should exit.
262
-
263
- Uses the configured exit condition:
264
- - valleys_plateau: Exit when valley count plateaus
265
- - entropy_drop: Exit when entropy drops below threshold
266
- - velocity_zero: Exit when velocity stabilizes
267
- - combined: Use all conditions with AND logic
268
- - repetition: Exit when model starts repeating
269
- - confidence: Exit when confidence is high for N tokens
270
- """
271
- # Always require minimum tokens
272
- if len(self._trajectory) < self.config.min_tokens:
273
- return False
274
-
275
- # Always require minimum valleys (for most strategies)
276
- condition = self.config.exit_condition
277
-
278
- if condition == ExitCondition.REPETITION:
279
- # Repetition doesn't require valleys
280
- return self.is_repeating()
281
-
282
- if condition == ExitCondition.CONFIDENCE:
283
- # Confidence doesn't require valleys
284
- return self.is_confident()
285
-
286
- # For other strategies, require minimum valleys
287
- if len(self._valleys) < self.config.min_valleys:
288
- return False
289
-
290
- if condition == ExitCondition.VALLEYS_PLATEAU:
291
- return self.is_valleys_plateau()
292
-
293
- if condition == ExitCondition.ENTROPY_DROP:
294
- return self.is_entropy_low()
295
-
296
- if condition == ExitCondition.VELOCITY_ZERO:
297
- return self.is_velocity_stable()
298
-
299
- if condition == ExitCondition.COMBINED:
300
- # Combined: require entropy low OR valleys plateau, AND velocity stable
301
- return (self.is_entropy_low() or self.is_valleys_plateau()) and self.is_velocity_stable()
302
-
303
- if condition == ExitCondition.SEMANTIC:
304
- # Placeholder for future semantic convergence detection
305
- # Would use embeddings to detect when output stabilizes semantically
306
- return False
307
-
308
- return False
309
-
310
- def is_converged(self) -> bool:
311
- """Alias for should_exit()."""
312
- return self.should_exit()
313
-
314
- def get_trajectory(self) -> List[float]:
315
- """Get full entropy trajectory as list of floats."""
316
- return [p.entropy for p in self._trajectory]
317
-
318
- def get_tokens(self) -> List[str]:
319
- """Get all tracked tokens."""
320
- return [p.token for p in self._trajectory]
321
-
322
- def get_stats(self) -> Dict[str, Any]:
323
- """Get summary statistics."""
324
- if not self._trajectory:
325
- return {}
326
-
327
- entropies = [p.entropy for p in self._trajectory]
328
- return {
329
- "token_count": len(self._trajectory),
330
- "valley_count": len(self._valleys),
331
- "mean_entropy": sum(entropies) / len(entropies),
332
- "min_entropy": min(entropies),
333
- "max_entropy": max(entropies),
334
- "current_entropy": entropies[-1],
335
- "current_velocity": self.get_velocity(),
336
- "is_converged": self.should_exit(),
337
- "exit_reason": self._get_exit_reason(),
338
- }
339
-
340
- def _get_exit_reason(self) -> Optional[str]:
341
- """Get the reason for early exit (if triggered)."""
342
- if not self.should_exit():
343
- return None
344
-
345
- condition = self.config.exit_condition
346
-
347
- if condition == ExitCondition.REPETITION:
348
- return "repetition_detected"
349
- if condition == ExitCondition.CONFIDENCE:
350
- return "high_confidence"
351
- if condition == ExitCondition.ENTROPY_DROP:
352
- return "entropy_below_threshold"
353
- if condition == ExitCondition.VELOCITY_ZERO:
354
- return "velocity_stable"
355
- if condition == ExitCondition.VALLEYS_PLATEAU:
356
- return "valleys_plateau"
357
- if condition == ExitCondition.COMBINED:
358
- if self.is_entropy_low() and self.is_velocity_stable():
359
- return "entropy_low_velocity_stable"
360
- if self.is_valleys_plateau() and self.is_velocity_stable():
361
- return "valleys_plateau_velocity_stable"
362
- return "combined"
363
-
364
- return "unknown"
365
-
366
- def reset(self) -> None:
367
- """Clear all tracked data."""
368
- self._trajectory.clear()
369
- self._valleys.clear()
370
- self._index = 0
371
- self._high_confidence_count = 0
372
-
373
-
374
- # Convenience function for one-shot entropy calculation
375
- def calculate_entropy_from_logprobs(logprobs: List[float]) -> float:
376
- """
377
- Calculate Shannon entropy from log probabilities.
378
-
379
- Args:
380
- logprobs: List of log probabilities (natural log)
381
-
382
- Returns:
383
- Shannon entropy in bits
384
- """
385
- entropy = 0.0
386
- for lp in logprobs:
387
- prob = math.exp(lp)
388
- if prob > 0:
389
- entropy -= prob * math.log2(prob + 1e-10)
390
- return entropy
1
+ """
2
+ Entropy Monitor — Core entropy tracking and early exit logic.
3
+
4
+ Supports multiple exit strategies:
5
+ - Valleys plateau: Exit when reasoning milestones stabilize
6
+ - Entropy drop: Exit when model confidence is high
7
+ - Velocity zero: Exit when entropy stops changing
8
+ - Combined: Multiple conditions with AND/OR logic
9
+ - Repetition: Exit when model starts repeating
10
+ - Confidence: Exit when top token probability > threshold for N tokens
11
+ """
12
+
13
+ import math
14
+ from typing import List, Tuple, Optional, Dict, Any, Callable
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ from collections import Counter
18
+
19
+
20
+ class ExitCondition(Enum):
21
+ VALLEYS_PLATEAU = "valleys_plateau"
22
+ ENTROPY_DROP = "entropy_drop"
23
+ VELOCITY_ZERO = "velocity_zero"
24
+ COMBINED = "combined"
25
+ # New strategies
26
+ REPETITION = "repetition"
27
+ CONFIDENCE = "confidence"
28
+ SEMANTIC = "semantic"
29
+
30
+
31
+ @dataclass
32
+ class EntropyPoint:
33
+ """A single point in the entropy trajectory."""
34
+ index: int
35
+ token: str
36
+ entropy: float
37
+ is_valley: bool = False
38
+ velocity: float = 0.0
39
+ confidence: float = 0.0 # Top token probability
40
+
41
+
42
+ @dataclass
43
+ class MonitorConfig:
44
+ """Configuration for the entropy monitor."""
45
+ entropy_threshold: float = 0.15
46
+ min_valleys: int = 2
47
+ velocity_threshold: float = 0.05
48
+ min_tokens: int = 50
49
+ valley_window: int = 5
50
+ plateau_threshold: int = 3
51
+ exit_condition: ExitCondition = ExitCondition.COMBINED
52
+ # New config options
53
+ repetition_window: int = 20 # Window to check for repetition
54
+ repetition_threshold: float = 0.3 # 30% repetition = exit
55
+ confidence_threshold: float = 0.95 # 95% confidence = exit
56
+ confidence_min_tokens: int = 5 # Min tokens at high confidence
57
+
58
+
59
+ class EntropyMonitor:
60
+ """
61
+ Monitor entropy trajectory and detect reasoning convergence.
62
+
63
+ Usage:
64
+ monitor = EntropyMonitor()
65
+ for token, entropy in stream:
66
+ monitor.track(token, entropy)
67
+ if monitor.should_exit():
68
+ break
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ entropy_threshold: float = 0.15,
74
+ min_valleys: int = 2,
75
+ velocity_threshold: float = 0.05,
76
+ min_tokens: int = 50,
77
+ valley_window: int = 5,
78
+ plateau_threshold: int = 3,
79
+ exit_condition: str = "combined",
80
+ # New parameters
81
+ repetition_window: int = 20,
82
+ repetition_threshold: float = 0.3,
83
+ confidence_threshold: float = 0.95,
84
+ confidence_min_tokens: int = 5,
85
+ ):
86
+ self.config = MonitorConfig(
87
+ entropy_threshold=entropy_threshold,
88
+ min_valleys=min_valleys,
89
+ velocity_threshold=velocity_threshold,
90
+ min_tokens=min_tokens,
91
+ valley_window=valley_window,
92
+ plateau_threshold=plateau_threshold,
93
+ exit_condition=ExitCondition(exit_condition),
94
+ repetition_window=repetition_window,
95
+ repetition_threshold=repetition_threshold,
96
+ confidence_threshold=confidence_threshold,
97
+ confidence_min_tokens=confidence_min_tokens,
98
+ )
99
+ self._trajectory: List[EntropyPoint] = []
100
+ self._valleys: List[EntropyPoint] = []
101
+ self._index = 0
102
+ self._high_confidence_count = 0 # Track consecutive high confidence
103
+
104
+ def calculate_entropy(self, logprobs: List[float], from_probs: bool = False) -> float:
105
+ """
106
+ Calculate Shannon entropy from log probabilities or probabilities.
107
+
108
+ Args:
109
+ logprobs: List of log probabilities (natural log) or probabilities
110
+ from_probs: If True, treat input as probabilities (will convert)
111
+
112
+ Returns:
113
+ Shannon entropy in bits
114
+ """
115
+ if not logprobs:
116
+ return 0.0
117
+
118
+ entropy = 0.0
119
+ for lp in logprobs:
120
+ if from_probs:
121
+ prob = lp
122
+ else:
123
+ prob = math.exp(lp)
124
+ if prob > 0:
125
+ entropy -= prob * math.log2(prob + 1e-10)
126
+
127
+ return entropy
128
+
129
+ def track(self, token: str, entropy: float, confidence: float = 0.0) -> EntropyPoint:
130
+ """
131
+ Track a token and its entropy value.
132
+
133
+ Args:
134
+ token: The generated token
135
+ entropy: Calculated entropy for this token
136
+ confidence: Top token probability (optional, for confidence strategy)
137
+
138
+ Returns:
139
+ EntropyPoint with valley detection
140
+ """
141
+ point = EntropyPoint(
142
+ index=self._index,
143
+ token=token,
144
+ entropy=entropy,
145
+ confidence=confidence
146
+ )
147
+
148
+ # Calculate velocity
149
+ if len(self._trajectory) > 0:
150
+ prev = self._trajectory[-1]
151
+ point.velocity = abs(entropy - prev.entropy)
152
+
153
+ # Detect valley (local minimum)
154
+ if len(self._trajectory) >= 2:
155
+ prev2 = self._trajectory[-2]
156
+ prev1 = self._trajectory[-1]
157
+ if prev1.entropy < prev2.entropy and prev1.entropy < entropy:
158
+ prev1.is_valley = True
159
+ self._valleys.append(prev1)
160
+
161
+ self._trajectory.append(point)
162
+ self._index += 1
163
+
164
+ # Track high confidence
165
+ if confidence >= self.config.confidence_threshold:
166
+ self._high_confidence_count += 1
167
+ else:
168
+ self._high_confidence_count = 0
169
+
170
+ return point
171
+
172
+ def get_valleys(self) -> List[Tuple[int, float]]:
173
+ """Get all entropy valleys (local minima) as (index, entropy) tuples."""
174
+ return [(v.index, v.entropy) for v in self._valleys]
175
+
176
+ def get_velocity(self) -> float:
177
+ """Get current entropy velocity (rate of change)."""
178
+ if len(self._trajectory) < 2:
179
+ return 0.0
180
+ return self._trajectory[-1].velocity
181
+
182
+ def get_mean_entropy(self) -> float:
183
+ """Get mean entropy over the trajectory."""
184
+ if not self._trajectory:
185
+ return 0.0
186
+ return sum(p.entropy for p in self._trajectory) / len(self._trajectory)
187
+
188
+ def get_valley_count(self) -> int:
189
+ """Get the number of detected valleys."""
190
+ return len(self._valleys)
191
+
192
+ def is_valleys_plateau(self) -> bool:
193
+ """Check if valley count has plateaued."""
194
+ if len(self._valleys) < self.config.min_valleys:
195
+ return False
196
+
197
+ # Check if last N valleys have similar spacing
198
+ recent = self._valleys[-self.config.plateau_threshold:]
199
+ if len(recent) < self.config.plateau_threshold:
200
+ return False
201
+
202
+ # Calculate spacing between recent valleys
203
+ spacings = [
204
+ recent[i + 1].index - recent[i].index
205
+ for i in range(len(recent) - 1)
206
+ ]
207
+ if not spacings:
208
+ return False
209
+
210
+ mean_spacing = sum(spacings) / len(spacings)
211
+ variance = sum((s - mean_spacing) ** 2 for s in spacings) / len(spacings)
212
+
213
+ # Low variance in spacing = plateau
214
+ return variance < 10 # Threshold tuned empirically
215
+
216
+ def is_entropy_low(self) -> bool:
217
+ """Check if current entropy is below threshold."""
218
+ if not self._trajectory:
219
+ return False
220
+ return self._trajectory[-1].entropy < self.config.entropy_threshold
221
+
222
+ def is_velocity_stable(self) -> bool:
223
+ """Check if velocity is below threshold."""
224
+ return self.get_velocity() < self.config.velocity_threshold
225
+
226
+ def is_repeating(self) -> bool:
227
+ """
228
+ Check if the model is repeating itself.
229
+
230
+ Returns True if the repetition ratio in the recent window
231
+ exceeds the threshold.
232
+ """
233
+ if len(self._trajectory) < self.config.repetition_window:
234
+ return False
235
+
236
+ # Get recent tokens
237
+ recent_tokens = [
238
+ p.token for p in self._trajectory[-self.config.repetition_window :]
239
+ ]
240
+
241
+ # Count unique vs total
242
+ counter = Counter(recent_tokens)
243
+ unique_count = len(counter)
244
+ total_count = len(recent_tokens)
245
+
246
+ # Calculate repetition ratio
247
+ repetition_ratio = 1.0 - (unique_count / total_count)
248
+
249
+ return repetition_ratio >= self.config.repetition_threshold
250
+
251
+ def is_confident(self) -> bool:
252
+ """
253
+ Check if model has been highly confident for consecutive tokens.
254
+
255
+ Returns True if the last N tokens had confidence >= threshold.
256
+ """
257
+ return self._high_confidence_count >= self.config.confidence_min_tokens
258
+
259
+ def should_exit(self) -> bool:
260
+ """
261
+ Determine if reasoning has converged and we should exit.
262
+
263
+ Uses the configured exit condition:
264
+ - valleys_plateau: Exit when valley count plateaus
265
+ - entropy_drop: Exit when entropy drops below threshold
266
+ - velocity_zero: Exit when velocity stabilizes
267
+ - combined: Use all conditions with AND logic
268
+ - repetition: Exit when model starts repeating
269
+ - confidence: Exit when confidence is high for N tokens
270
+ """
271
+ # Always require minimum tokens
272
+ if len(self._trajectory) < self.config.min_tokens:
273
+ return False
274
+
275
+ # Always require minimum valleys (for most strategies)
276
+ condition = self.config.exit_condition
277
+
278
+ if condition == ExitCondition.REPETITION:
279
+ # Repetition doesn't require valleys
280
+ return self.is_repeating()
281
+
282
+ if condition == ExitCondition.CONFIDENCE:
283
+ # Confidence doesn't require valleys
284
+ return self.is_confident()
285
+
286
+ # For other strategies, require minimum valleys
287
+ if len(self._valleys) < self.config.min_valleys:
288
+ return False
289
+
290
+ if condition == ExitCondition.VALLEYS_PLATEAU:
291
+ return self.is_valleys_plateau()
292
+
293
+ if condition == ExitCondition.ENTROPY_DROP:
294
+ return self.is_entropy_low()
295
+
296
+ if condition == ExitCondition.VELOCITY_ZERO:
297
+ return self.is_velocity_stable()
298
+
299
+ if condition == ExitCondition.COMBINED:
300
+ # Combined: require entropy low OR valleys plateau, AND velocity stable
301
+ return (self.is_entropy_low() or self.is_valleys_plateau()) and self.is_velocity_stable()
302
+
303
+ if condition == ExitCondition.SEMANTIC:
304
+ # Placeholder for future semantic convergence detection
305
+ # Would use embeddings to detect when output stabilizes semantically
306
+ return False
307
+
308
+ return False
309
+
310
+ def is_converged(self) -> bool:
311
+ """Alias for should_exit()."""
312
+ return self.should_exit()
313
+
314
+ def get_trajectory(self) -> List[float]:
315
+ """Get full entropy trajectory as list of floats."""
316
+ return [p.entropy for p in self._trajectory]
317
+
318
+ def get_tokens(self) -> List[str]:
319
+ """Get all tracked tokens."""
320
+ return [p.token for p in self._trajectory]
321
+
322
+ def get_stats(self) -> Dict[str, Any]:
323
+ """Get summary statistics."""
324
+ if not self._trajectory:
325
+ return {}
326
+
327
+ entropies = [p.entropy for p in self._trajectory]
328
+ return {
329
+ "token_count": len(self._trajectory),
330
+ "valley_count": len(self._valleys),
331
+ "mean_entropy": sum(entropies) / len(entropies),
332
+ "min_entropy": min(entropies),
333
+ "max_entropy": max(entropies),
334
+ "current_entropy": entropies[-1],
335
+ "current_velocity": self.get_velocity(),
336
+ "is_converged": self.should_exit(),
337
+ "exit_reason": self._get_exit_reason(),
338
+ }
339
+
340
+ def _get_exit_reason(self) -> Optional[str]:
341
+ """Get the reason for early exit (if triggered)."""
342
+ if not self.should_exit():
343
+ return None
344
+
345
+ condition = self.config.exit_condition
346
+
347
+ if condition == ExitCondition.REPETITION:
348
+ return "repetition_detected"
349
+ if condition == ExitCondition.CONFIDENCE:
350
+ return "high_confidence"
351
+ if condition == ExitCondition.ENTROPY_DROP:
352
+ return "entropy_below_threshold"
353
+ if condition == ExitCondition.VELOCITY_ZERO:
354
+ return "velocity_stable"
355
+ if condition == ExitCondition.VALLEYS_PLATEAU:
356
+ return "valleys_plateau"
357
+ if condition == ExitCondition.COMBINED:
358
+ if self.is_entropy_low() and self.is_velocity_stable():
359
+ return "entropy_low_velocity_stable"
360
+ if self.is_valleys_plateau() and self.is_velocity_stable():
361
+ return "valleys_plateau_velocity_stable"
362
+ return "combined"
363
+
364
+ return "unknown"
365
+
366
+ def reset(self) -> None:
367
+ """Clear all tracked data."""
368
+ self._trajectory.clear()
369
+ self._valleys.clear()
370
+ self._index = 0
371
+ self._high_confidence_count = 0
372
+
373
+
374
+ # Convenience function for one-shot entropy calculation
375
+ def calculate_entropy_from_logprobs(logprobs: List[float]) -> float:
376
+ """
377
+ Calculate Shannon entropy from log probabilities.
378
+
379
+ Args:
380
+ logprobs: List of log probabilities (natural log)
381
+
382
+ Returns:
383
+ Shannon entropy in bits
384
+ """
385
+ entropy = 0.0
386
+ for lp in logprobs:
387
+ prob = math.exp(lp)
388
+ if prob > 0:
389
+ entropy -= prob * math.log2(prob + 1e-10)
390
+ return entropy