Skip to content

rnel.betting

Betting (e-value) reading of a neutrosophic triplet built from several sources (EXPERIMENTAL).

Reference: Leyva-Vázquez, M. Y. and Smarandache, F., "What does T = 0.7 mean? A betting reading of neutrosophic triplets built from several sources, with anytime error guarantees and its limits" (manuscript, 2026).

Setting. A proposition A is examined by sources that arrive one after another. For every source j there are two e-values: e_j+ against not-A and e_j- against A (non-negative, with expectation at most 1 under the hypothesis they bet against). Two bettors start with capital 1. Before every source each stakes a fixed fraction lam of its current capital, the first against not-A and the second against A:

K+ <- K+ * (1 - lam + lam * e+)        K- <- K- * (1 - lam + lam * e-)

The triplet is read off the two capitals:

T = max(0, 1 - 1/K+)       F = max(0, 1 - 1/K-)
conflict   I_con = T * F                (both bettors have won)
ignorance  I_ign = (1 - T) * (1 - F)    (neither has)
I = I_con + I_ign,  so  T + I + F = 1 + 2 T F

Guarantee (Ville's inequality). If not-A holds, P(T ever reaches 1 - alpha) <= alpha, however often the triplet is recomputed; symmetrically for F under A. 1 - T = min(1, 1/K+) is an anytime-valid p-value.

Verdicts at level alpha: "supported" (T >= 1 - alpha > F), "refuted" (F >= 1 - alpha > T), "conflict" (both), "insufficient" (neither). Record verdicts use the running maxima T* and F*.

Facts worth knowing before using it
  • lam = 1 (pooling) never registers conflict: K+ * K- <= 1 when e+ * e- <= 1, so T and F cannot both be > 0.
  • With lam < 1 the state depends on lam, on the effect size the e-values are built on, and on how the evidence is divided into sources. Fix them before seeing the data and report a grid.
  • The verdicts are existential: "supported" rejects "no source is governed by A"; it does not say that A holds for every source.
  • The guarantee presupposes valid e-values: unbiased, independent sources and a correct model.
Two families of e-values are provided

point_evalues two point hypotheses: e+ = likelihood ratio, e- = its inverse gaussian_evalues complementary one-sided hypotheses about a normal estimate (y, s) with a design effect delta: A is "theta > theta0" (or "< theta0"); conflict then means sign disagreement

and helpers that turn 2x2 tables or published hazard ratios into (y, s).

Refined truth (Section 10 of the manuscript, contributed by F. Smarandache), after the n-valued refined neutrosophic set (Smarandache 2013), with the sources read as possible worlds:

refined_triplet sextuple (T_abs, T_rel; I_con, I_ign; F_rel, F_abs). T_rel is the betting T ("true in at least one source"); T_abs = tau(min_j e_j+) is "true in every source", with P(false T_abs >= 1 - alpha) <= alpha (Proposition 9); T_abs > 0 forces F = 0 when e+ * e- <= 1 (Proposition 10). graded_truth sub-truths T_k betting against "every theta_j <= theta0 + eta_k" and their merge by the weighted MEAN of the capitals, which keeps the guarantee under any dependence (Proposition 11). Taking max_k T_k (a t-conorm) is valid only at level p * alpha.

Pure Python (standard library only). All capitals are kept on the log scale.

VERDICTS = ('insufficient', 'supported', 'refuted', 'conflict') module-attribute

DEFAULT_GRID = (0.1, 0.25, 0.5, 0.75, 1.0) module-attribute

REFINED_VERDICTS = ('insufficient', 'supported in some source', 'supported in every source', 'refuted in some source', 'refuted in every source', 'conflict') module-attribute

BettingState dataclass

Pair of betting capitals with a constant stake fraction lam, updated source by source.

st = BettingState(lam=0.5) st.update(math.log(1000), -math.log(1000)).update(-math.log(1000), math.log(1000)) # doctest: +ELLIPSIS BettingState(...) round(st.T, 3), round(st.F, 3), st.verdict() (0.996, 0.996, 'conflict')

Source code in src/rnel/betting.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
@dataclass
class BettingState:
    """Pair of betting capitals with a constant stake fraction `lam`, updated source by source.

    >>> st = BettingState(lam=0.5)
    >>> st.update(math.log(1000), -math.log(1000)).update(-math.log(1000), math.log(1000))  # doctest: +ELLIPSIS
    BettingState(...)
    >>> round(st.T, 3), round(st.F, 3), st.verdict()
    (0.996, 0.996, 'conflict')
    """

    lam: float = 0.5
    log_kp: float = 0.0
    log_km: float = 0.0
    log_kp_max: float = 0.0
    log_km_max: float = 0.0
    n: int = 0
    path: list = field(default_factory=list, repr=False)

    def __post_init__(self):
        if not 0.0 <= self.lam <= 1.0:
            raise ValueError("lam must be in [0, 1]")

    def update(self, log_e_plus: float, log_e_minus: float) -> "BettingState":
        """Add one source, given its two e-values on the log scale."""
        self.log_kp += log_factor(log_e_plus, self.lam)
        self.log_km += log_factor(log_e_minus, self.lam)
        self.log_kp_max, self.log_km_max = max(self.log_kp_max, self.log_kp), max(self.log_km_max, self.log_km)
        self.n += 1
        self.path.append((self.T, self.F))
        return self

    def extend(self, log_e_plus: Iterable[float], log_e_minus: Iterable[float]) -> "BettingState":
        for lp, lm in zip(log_e_plus, log_e_minus, strict=True):
            self.update(lp, lm)
        return self

    # read-out ---------------------------------------------------------------------------------------------------
    @property
    def K_plus(self) -> float:
        return math.exp(self.log_kp)

    @property
    def K_minus(self) -> float:
        return math.exp(self.log_km)

    @property
    def T(self) -> float:
        return tf_from_log_capital(self.log_kp)

    @property
    def F(self) -> float:
        return tf_from_log_capital(self.log_km)

    @property
    def T_record(self) -> float:
        return tf_from_log_capital(self.log_kp_max)

    @property
    def F_record(self) -> float:
        return tf_from_log_capital(self.log_km_max)

    @property
    def conflict(self) -> float:
        return self.T * self.F

    @property
    def ignorance(self) -> float:
        return (1 - self.T) * (1 - self.F)

    @property
    def I(self) -> float:  # noqa: E743 (the neutrosophic name)
        return self.conflict + self.ignorance

    def triplet(self) -> tuple[float, float, float]:
        """(T, I, F)."""
        return self.T, self.I, self.F

    def masses(self) -> dict:
        """Belnap masses: true-only T(1-F), false-only (1-T)F, both (conflict), neither (ignorance)."""
        T, F = self.T, self.F
        return {"true": T * (1 - F), "false": (1 - T) * F, "both": T * F, "neither": (1 - T) * (1 - F)}

    def p_values(self) -> tuple[float, float]:
        """Anytime-valid p-values (1 - T, 1 - F) = (min(1, 1/K+), min(1, 1/K-))."""
        return 1 - self.T, 1 - self.F

    def verdict(self, alpha: float = 0.05) -> str:
        return verdict(self.T, self.F, alpha)

    def record_verdict(self, alpha: float = 0.05) -> str:
        return verdict(self.T_record, self.F_record, alpha)

    def first_crossing(self, alpha: float = 0.05, side: str = "T") -> int | None:
        """1-based index of the first source after which T (or F) reached 1 - alpha; None if never."""
        k = 0 if side == "T" else 1
        for i, tf in enumerate(self.path, start=1):
            if tf[k] >= 1 - alpha:
                return i
        return None

lam = 0.5 class-attribute instance-attribute

log_kp = 0.0 class-attribute instance-attribute

log_km = 0.0 class-attribute instance-attribute

log_kp_max = 0.0 class-attribute instance-attribute

log_km_max = 0.0 class-attribute instance-attribute

n = 0 class-attribute instance-attribute

path = field(default_factory=list, repr=False) class-attribute instance-attribute

K_plus property

K_minus property

T property

F property

T_record property

F_record property

conflict property

ignorance property

I property

update(log_e_plus, log_e_minus)

Add one source, given its two e-values on the log scale.

Source code in src/rnel/betting.py
237
238
239
240
241
242
243
244
def update(self, log_e_plus: float, log_e_minus: float) -> "BettingState":
    """Add one source, given its two e-values on the log scale."""
    self.log_kp += log_factor(log_e_plus, self.lam)
    self.log_km += log_factor(log_e_minus, self.lam)
    self.log_kp_max, self.log_km_max = max(self.log_kp_max, self.log_kp), max(self.log_km_max, self.log_km)
    self.n += 1
    self.path.append((self.T, self.F))
    return self

extend(log_e_plus, log_e_minus)

Source code in src/rnel/betting.py
246
247
248
249
def extend(self, log_e_plus: Iterable[float], log_e_minus: Iterable[float]) -> "BettingState":
    for lp, lm in zip(log_e_plus, log_e_minus, strict=True):
        self.update(lp, lm)
    return self

triplet()

(T, I, F).

Source code in src/rnel/betting.py
288
289
290
def triplet(self) -> tuple[float, float, float]:
    """(T, I, F)."""
    return self.T, self.I, self.F

masses()

Belnap masses: true-only T(1-F), false-only (1-T)F, both (conflict), neither (ignorance).

Source code in src/rnel/betting.py
292
293
294
295
def masses(self) -> dict:
    """Belnap masses: true-only T(1-F), false-only (1-T)F, both (conflict), neither (ignorance)."""
    T, F = self.T, self.F
    return {"true": T * (1 - F), "false": (1 - T) * F, "both": T * F, "neither": (1 - T) * (1 - F)}

p_values()

Anytime-valid p-values (1 - T, 1 - F) = (min(1, 1/K+), min(1, 1/K-)).

Source code in src/rnel/betting.py
297
298
299
def p_values(self) -> tuple[float, float]:
    """Anytime-valid p-values (1 - T, 1 - F) = (min(1, 1/K+), min(1, 1/K-))."""
    return 1 - self.T, 1 - self.F

verdict(alpha=0.05)

Source code in src/rnel/betting.py
301
302
def verdict(self, alpha: float = 0.05) -> str:
    return verdict(self.T, self.F, alpha)

record_verdict(alpha=0.05)

Source code in src/rnel/betting.py
304
305
def record_verdict(self, alpha: float = 0.05) -> str:
    return verdict(self.T_record, self.F_record, alpha)

first_crossing(alpha=0.05, side='T')

1-based index of the first source after which T (or F) reached 1 - alpha; None if never.

Source code in src/rnel/betting.py
307
308
309
310
311
312
313
def first_crossing(self, alpha: float = 0.05, side: str = "T") -> int | None:
    """1-based index of the first source after which T (or F) reached 1 - alpha; None if never."""
    k = 0 if side == "T" else 1
    for i, tf in enumerate(self.path, start=1):
        if tf[k] >= 1 - alpha:
            return i
    return None

MixtureState dataclass

Uniform (or weighted) mixture over stake fractions: K = sum_i w_i K_{lam_i}. It keeps the guarantee of Ville's inequality, so the triplet, verdicts and p-values are read exactly as for BettingState.

Source code in src/rnel/betting.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
@dataclass
class MixtureState:
    """Uniform (or weighted) mixture over stake fractions: K = sum_i w_i K_{lam_i}. It keeps the guarantee of
    Ville's inequality, so the triplet, verdicts and p-values are read exactly as for `BettingState`."""

    grid: tuple = DEFAULT_GRID
    weights: tuple | None = None
    states: list = field(default_factory=list, repr=False)
    path: list = field(default_factory=list, repr=False)
    log_kp_max: float = 0.0
    log_km_max: float = 0.0

    def __post_init__(self):
        w = self.weights or tuple(1.0 / len(self.grid) for _ in self.grid)
        if len(w) != len(self.grid) or any(x < 0 for x in w) or abs(sum(w) - 1) > 1e-9:
            raise ValueError("weights must be non-negative, one per grid value, summing to 1")
        self.weights = tuple(w)
        self.states = [BettingState(lam=l) for l in self.grid]

    def update(self, log_e_plus: float, log_e_minus: float) -> "MixtureState":
        for st in self.states:
            st.update(log_e_plus, log_e_minus)
        self.log_kp_max, self.log_km_max = max(self.log_kp_max, self.log_kp), max(self.log_km_max, self.log_km)
        self.path.append((self.T, self.F))
        return self

    def extend(self, log_e_plus: Iterable[float], log_e_minus: Iterable[float]) -> "MixtureState":
        for lp, lm in zip(log_e_plus, log_e_minus, strict=True):
            self.update(lp, lm)
        return self

    def _mix(self, attr):
        return _logsumexp([math.log(w) + getattr(st, attr) for w, st in zip(self.weights, self.states) if w > 0])

    @property
    def log_kp(self) -> float:
        return self._mix("log_kp")

    @property
    def log_km(self) -> float:
        return self._mix("log_km")

    @property
    def n(self) -> int:
        return self.states[0].n

    # same read-out as BettingState
    K_plus = BettingState.K_plus
    K_minus = BettingState.K_minus
    T = BettingState.T
    F = BettingState.F
    T_record = BettingState.T_record
    F_record = BettingState.F_record
    conflict = BettingState.conflict
    ignorance = BettingState.ignorance
    I = BettingState.I  # noqa: E741
    triplet = BettingState.triplet
    masses = BettingState.masses
    p_values = BettingState.p_values
    verdict = BettingState.verdict
    record_verdict = BettingState.record_verdict
    first_crossing = BettingState.first_crossing

grid = DEFAULT_GRID class-attribute instance-attribute

weights = None class-attribute instance-attribute

states = field(default_factory=list, repr=False) class-attribute instance-attribute

path = field(default_factory=list, repr=False) class-attribute instance-attribute

log_kp_max = 0.0 class-attribute instance-attribute

log_km_max = 0.0 class-attribute instance-attribute

log_kp property

log_km property

n property

K_plus = BettingState.K_plus class-attribute instance-attribute

K_minus = BettingState.K_minus class-attribute instance-attribute

T = BettingState.T class-attribute instance-attribute

F = BettingState.F class-attribute instance-attribute

T_record = BettingState.T_record class-attribute instance-attribute

F_record = BettingState.F_record class-attribute instance-attribute

conflict = BettingState.conflict class-attribute instance-attribute

ignorance = BettingState.ignorance class-attribute instance-attribute

I = BettingState.I class-attribute instance-attribute

triplet = BettingState.triplet class-attribute instance-attribute

masses = BettingState.masses class-attribute instance-attribute

p_values = BettingState.p_values class-attribute instance-attribute

verdict = BettingState.verdict class-attribute instance-attribute

record_verdict = BettingState.record_verdict class-attribute instance-attribute

first_crossing = BettingState.first_crossing class-attribute instance-attribute

update(log_e_plus, log_e_minus)

Source code in src/rnel/betting.py
335
336
337
338
339
340
def update(self, log_e_plus: float, log_e_minus: float) -> "MixtureState":
    for st in self.states:
        st.update(log_e_plus, log_e_minus)
    self.log_kp_max, self.log_km_max = max(self.log_kp_max, self.log_kp), max(self.log_km_max, self.log_km)
    self.path.append((self.T, self.F))
    return self

extend(log_e_plus, log_e_minus)

Source code in src/rnel/betting.py
342
343
344
345
def extend(self, log_e_plus: Iterable[float], log_e_minus: Iterable[float]) -> "MixtureState":
    for lp, lm in zip(log_e_plus, log_e_minus, strict=True):
        self.update(lp, lm)
    return self

log_factor(log_e, lam)

log(1 - lam + lam * e) for an e-value given on the log scale; stable for very large or small e.

Source code in src/rnel/betting.py
105
106
107
108
109
110
111
112
113
def log_factor(log_e: float, lam: float) -> float:
    """log(1 - lam + lam * e) for an e-value given on the log scale; stable for very large or small e."""
    if not 0.0 <= lam <= 1.0:
        raise ValueError("lam must be in [0, 1]")
    if lam == 1.0:
        return float(log_e)
    if lam == 0.0:
        return 0.0
    return _logaddexp(math.log1p(-lam), math.log(lam) + log_e)

tf_from_log_capital(log_k)

T (or F) from a log capital: max(0, 1 - 1/K).

Source code in src/rnel/betting.py
116
117
118
119
120
def tf_from_log_capital(log_k: float) -> float:
    """T (or F) from a log capital: max(0, 1 - 1/K)."""
    if log_k <= 0.0:
        return 0.0
    return -math.expm1(-log_k)

verdict(T, F, alpha=0.05)

Level-alpha verdict: 'supported', 'refuted', 'conflict' or 'insufficient'.

Source code in src/rnel/betting.py
123
124
125
126
127
def verdict(T: float, F: float, alpha: float = 0.05) -> str:
    """Level-alpha verdict: 'supported', 'refuted', 'conflict' or 'insufficient'."""
    if not 0.0 < alpha < 1.0:
        raise ValueError("alpha must be in (0, 1)")
    return VERDICTS[int(T >= 1 - alpha) + 2 * int(F >= 1 - alpha)]

point_evalues(log_lr)

Two point hypotheses: log e+ = log LR(A : not-A), log e- = -log LR. Returns (log_e_plus, log_e_minus).

Source code in src/rnel/betting.py
134
135
136
137
def point_evalues(log_lr: Iterable[float]) -> tuple[list[float], list[float]]:
    """Two point hypotheses: log e+ = log LR(A : not-A), log e- = -log LR. Returns (log_e_plus, log_e_minus)."""
    lp = [float(x) for x in log_lr]
    return lp, [-x for x in lp]

gaussian_evalues(y, s, delta, theta0=0.0, direction='greater')

Complementary one-sided e-values for normal estimates y_j with standard errors s_j.

direction="greater": A is theta_j > theta0 for the sources it governs, not-A is theta_j <= theta0. direction="less": A is theta_j < theta0 (e.g. a log odds ratio below 0 meaning benefit). delta > 0 is the design effect: the distance from theta0 on which each bet is built. With b = +-(y - theta0),

log e+ =  delta * b / s^2 - delta^2 / (2 s^2)     (against not-A; valid for the composite null)
log e- = -delta * b / s^2 - delta^2 / (2 s^2)     (against A)

Returns (log_e_plus, log_e_minus).

Source code in src/rnel/betting.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def gaussian_evalues(y: Iterable[float], s: Iterable[float], delta: float, theta0: float = 0.0,
                     direction: str = "greater") -> tuple[list[float], list[float]]:
    """Complementary one-sided e-values for normal estimates y_j with standard errors s_j.

    direction="greater": A is theta_j > theta0 for the sources it governs, not-A is theta_j <= theta0.
    direction="less":    A is theta_j < theta0 (e.g. a log odds ratio below 0 meaning benefit).
    delta > 0 is the design effect: the distance from theta0 on which each bet is built. With b = +-(y - theta0),

        log e+ =  delta * b / s^2 - delta^2 / (2 s^2)     (against not-A; valid for the composite null)
        log e- = -delta * b / s^2 - delta^2 / (2 s^2)     (against A)

    Returns (log_e_plus, log_e_minus)."""
    if delta <= 0:
        raise ValueError("delta must be > 0")
    if direction not in ("greater", "less"):
        raise ValueError("direction must be 'greater' or 'less'")
    sign = 1.0 if direction == "greater" else -1.0
    lp, lm = [], []
    for yj, sj in zip(y, s, strict=True):
        if sj <= 0:
            raise ValueError("standard errors must be > 0")
        b, v = sign * (yj - theta0), sj * sj
        lp.append(delta * b / v - delta * delta / (2 * v))
        lm.append(-delta * b / v - delta * delta / (2 * v))
    return lp, lm

log_odds_ratio(a, n1, c, n2, correction=0.5)

(log OR, s.e.) of events a/n1 (treated) against c/n2 (control); correction is added to all four cells of a table with a zero cell.

Source code in src/rnel/betting.py
176
177
178
179
180
def log_odds_ratio(a: float, n1: float, c: float, n2: float, correction: float = 0.5) -> tuple[float, float]:
    """(log OR, s.e.) of events a/n1 (treated) against c/n2 (control); `correction` is added to all four cells
    of a table with a zero cell."""
    a, b, c, d = _cells(a, n1, c, n2, correction)
    return math.log(a * d / (b * c)), math.sqrt(1 / a + 1 / b + 1 / c + 1 / d)

log_risk_ratio(a, n1, c, n2, correction=0.5)

(log RR, s.e.) of events a/n1 against c/n2, with the same zero-cell rule as log_odds_ratio.

Source code in src/rnel/betting.py
183
184
185
186
187
def log_risk_ratio(a: float, n1: float, c: float, n2: float, correction: float = 0.5) -> tuple[float, float]:
    """(log RR, s.e.) of events a/n1 against c/n2, with the same zero-cell rule as `log_odds_ratio`."""
    a, b, c, d = _cells(a, n1, c, n2, correction)
    m1, m2 = a + b, c + d
    return math.log((a / m1) / (c / m2)), math.sqrt(1 / a - 1 / m1 + 1 / c - 1 / m2)

log_ratio_from_ci(estimate, lower, upper, level=0.95)

(log ratio, s.e.) from a published ratio (HR, OR, RR) and its symmetric-on-log confidence interval.

Source code in src/rnel/betting.py
190
191
192
193
194
195
def log_ratio_from_ci(estimate: float, lower: float, upper: float, level: float = 0.95) -> tuple[float, float]:
    """(log ratio, s.e.) from a published ratio (HR, OR, RR) and its symmetric-on-log confidence interval."""
    if not 0 < lower < estimate < upper:
        raise ValueError("need 0 < lower < estimate < upper")
    z = _Z975 if level == 0.95 else _norm_ppf(0.5 + level / 2)
    return math.log(estimate), (math.log(upper) - math.log(lower)) / (2 * z)

betting_triplet(log_e_plus, log_e_minus, lam=0.5, alpha=0.05)

One-call summary for a sequence of sources. lam is a stake fraction in [0, 1] or "mix" (uniform mixture over 0.1, 0.25, 0.5, 0.75, 1). Returns T, I, F, the conflict and ignorance parts, both capitals, the verdict, the record verdict, the first source at which T and F reached 1 - alpha, and the path of (T, F).

Source code in src/rnel/betting.py
380
381
382
383
384
385
386
387
388
389
390
391
def betting_triplet(log_e_plus: Iterable[float], log_e_minus: Iterable[float], lam: float | str = 0.5,
                    alpha: float = 0.05) -> dict:
    """One-call summary for a sequence of sources. `lam` is a stake fraction in [0, 1] or "mix" (uniform mixture
    over 0.1, 0.25, 0.5, 0.75, 1). Returns T, I, F, the conflict and ignorance parts, both capitals, the verdict,
    the record verdict, the first source at which T and F reached 1 - alpha, and the path of (T, F)."""
    st = MixtureState() if lam == "mix" else BettingState(lam=float(lam))
    st.extend(log_e_plus, log_e_minus)
    return {"T": st.T, "I": st.I, "F": st.F, "conflict": st.conflict, "ignorance": st.ignorance,
            "K_plus": st.K_plus, "K_minus": st.K_minus, "log_K_plus": st.log_kp, "log_K_minus": st.log_km,
            "verdict": st.verdict(alpha), "record_verdict": st.record_verdict(alpha),
            "first_T": st.first_crossing(alpha, "T"), "first_F": st.first_crossing(alpha, "F"),
            "n_sources": st.n, "path": list(st.path)}

absolute_degree(log_e)

Absolute degree tau(min_j e_j) = max(0, 1 - 1/min_j e_j) of a sequence of single-source log e-values: evidence that the hypothesis holds in EVERY source. It uses no stake, never increases as sources are added and is 0 for no sources. If (10) holds per source, a false value >= 1 - alpha has probability <= alpha (Prop. 9).

absolute_degree([math.log(30), math.log(8), math.log(0.5)]) 0.0 round(absolute_degree([math.log(25), math.log(30), math.log(40)]), 2) 0.96

Source code in src/rnel/betting.py
402
403
404
405
406
407
408
409
410
411
412
413
def absolute_degree(log_e: Iterable[float]) -> float:
    """Absolute degree tau(min_j e_j) = max(0, 1 - 1/min_j e_j) of a sequence of single-source log e-values:
    evidence that the hypothesis holds in EVERY source. It uses no stake, never increases as sources are added and
    is 0 for no sources. If (10) holds per source, a false value >= 1 - alpha has probability <= alpha (Prop. 9).

    >>> absolute_degree([math.log(30), math.log(8), math.log(0.5)])
    0.0
    >>> round(absolute_degree([math.log(25), math.log(30), math.log(40)]), 2)
    0.96
    """
    xs = [float(x) for x in log_e]
    return tf_from_log_capital(min(xs)) if xs else 0.0

refined_verdict(T_abs, T_rel, F_rel, F_abs, alpha=0.05)

Refined Definition 3: 'supported in every source' when T_abs >= 1 - alpha; 'supported in some source' when T_rel >= 1 - alpha > T_abs and F_rel < 1 - alpha; symmetrically for 'refuted'; 'conflict' when T_rel and F_rel both reach 1 - alpha; otherwise 'insufficient'.

Source code in src/rnel/betting.py
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
def refined_verdict(T_abs: float, T_rel: float, F_rel: float, F_abs: float, alpha: float = 0.05) -> str:
    """Refined Definition 3: 'supported in every source' when T_abs >= 1 - alpha; 'supported in some source' when
    T_rel >= 1 - alpha > T_abs and F_rel < 1 - alpha; symmetrically for 'refuted'; 'conflict' when T_rel and F_rel
    both reach 1 - alpha; otherwise 'insufficient'."""
    c = 1 - alpha
    if T_rel >= c and F_rel >= c:
        return "conflict"
    if T_abs >= c:
        return "supported in every source"
    if F_abs >= c:
        return "refuted in every source"
    if T_rel >= c:
        return "supported in some source"
    if F_rel >= c:
        return "refuted in some source"
    return "insufficient"

refined_triplet(log_e_plus, log_e_minus, lam=0.5, alpha=0.05)

Refined betting value (T_abs, T_rel; I_con, I_ign; F_rel, F_abs) of Definition 4, an n-valued refined neutrosophic value with p = r = s = 2. The gap between T_rel and T_abs is the number behind the warning that 'supported' means 'supported in at least one source'.

r = refined_triplet([math.log(30), math.log(8), math.log(0.5)], ... [-math.log(30), -math.log(8), -math.log(0.5)]) round(r["T_rel"], 3), r["T_abs"], r["verdict"] (0.981, 0.0, 'supported in some source')

Source code in src/rnel/betting.py
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
def refined_triplet(log_e_plus: Iterable[float], log_e_minus: Iterable[float], lam: float | str = 0.5,
                    alpha: float = 0.05) -> dict:
    """Refined betting value (T_abs, T_rel; I_con, I_ign; F_rel, F_abs) of Definition 4, an n-valued refined
    neutrosophic value with p = r = s = 2. The gap between T_rel and T_abs is the number behind the warning that
    'supported' means 'supported in at least one source'.

    >>> r = refined_triplet([math.log(30), math.log(8), math.log(0.5)],
    ...                     [-math.log(30), -math.log(8), -math.log(0.5)])
    >>> round(r["T_rel"], 3), r["T_abs"], r["verdict"]
    (0.981, 0.0, 'supported in some source')
    """
    lp, lm = [float(x) for x in log_e_plus], [float(x) for x in log_e_minus]
    base = betting_triplet(lp, lm, lam, alpha)
    T_abs, F_abs = absolute_degree(lp), absolute_degree(lm)
    T_rel, F_rel = base["T"], base["F"]
    return {"T_abs": T_abs, "T_rel": T_rel, "I_con": base["conflict"], "I_ign": base["ignorance"],
            "F_rel": F_rel, "F_abs": F_abs,
            "sextuple": (T_abs, T_rel, base["conflict"], base["ignorance"], F_rel, F_abs),
            "verdict": refined_verdict(T_abs, T_rel, F_rel, F_abs, alpha), "n_sources": base["n_sources"]}

merge_log_capitals(log_ks, weights=None)

Log of the weighted arithmetic mean of capitals (Proposition 11). The mean of e-values is valid under arbitrary dependence (Vovk and Wang 2021) and essentially the only admissible symmetric merge (Wang 2025).

Source code in src/rnel/betting.py
455
456
457
458
459
460
461
462
463
464
def merge_log_capitals(log_ks: Sequence[float], weights: Sequence[float] | None = None) -> float:
    """Log of the weighted arithmetic mean of capitals (Proposition 11). The mean of e-values is valid under
    arbitrary dependence (Vovk and Wang 2021) and essentially the only admissible symmetric merge (Wang 2025)."""
    ks = [float(x) for x in log_ks]
    if not ks:
        raise ValueError("no capitals to merge")
    w = [1.0 / len(ks)] * len(ks) if weights is None else [float(x) for x in weights]
    if len(w) != len(ks) or any(x < 0 for x in w) or abs(sum(w) - 1) > 1e-9:
        raise ValueError("weights must be non-negative, one per capital, and sum to one")
    return _logsumexp([math.log(wk) + k for wk, k in zip(w, ks) if wk > 0])

graded_truth(y, s, etas, delta, theta0=0.0, direction='greater', lam=0.5, weights=None, alpha=0.05)

Graded sub-truths of Section 10.3 for normal estimates. T_k bets against 'every theta_j <= theta0 + eta_k' (direction 'greater'; 'every theta_j >= theta0 - eta_k' for 'less'), so it is a relative truth that grades the size of the effect, with thresholds 0 = eta_1 < ... fixed before the data. T_bar = tau(mean of the capitals) keeps Theorem 1 against not-A; simultaneous statements about all T_k need level alpha / p each.

Returns T_k, their log capitals, T_bar and whether each reaches 1 - alpha.

Source code in src/rnel/betting.py
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
def graded_truth(y: Iterable[float], s: Iterable[float], etas: Sequence[float], delta: float, theta0: float = 0.0,
                 direction: str = "greater", lam: float = 0.5, weights: Sequence[float] | None = None,
                 alpha: float = 0.05) -> dict:
    """Graded sub-truths of Section 10.3 for normal estimates. T_k bets against 'every theta_j <= theta0 + eta_k'
    (direction 'greater'; 'every theta_j >= theta0 - eta_k' for 'less'), so it is a relative truth that grades the
    size of the effect, with thresholds 0 = eta_1 < ... fixed before the data. T_bar = tau(mean of the capitals)
    keeps Theorem 1 against not-A; simultaneous statements about all T_k need level alpha / p each.

    Returns T_k, their log capitals, T_bar and whether each reaches 1 - alpha."""
    ys, ss = [float(v) for v in y], [float(v) for v in s]
    if list(etas) != sorted(etas) or etas[0] < 0:
        raise ValueError("etas must be non-negative and increasing")
    sign = 1.0 if direction == "greater" else -1.0
    log_ks, Ts = [], []
    for eta in etas:
        lp, lm = gaussian_evalues(ys, ss, delta, theta0 + sign * eta, direction)
        st = BettingState(lam=lam).extend(lp, lm)
        log_ks.append(st.log_kp)
        Ts.append(st.T)
    log_bar = merge_log_capitals(log_ks, weights)
    T_bar = tf_from_log_capital(log_bar)
    c = 1 - alpha
    return {"etas": list(etas), "T_k": Ts, "log_K_k": log_ks, "T_bar": T_bar, "log_K_bar": log_bar,
            "reached": [t >= c for t in Ts], "T_bar_reached": T_bar >= c}