annotuneagent 0.1.0__py3-none-any.whl
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.
- annotuneagent/__init__.py +68 -0
- annotuneagent/__main__.py +6 -0
- annotuneagent/_version.py +19 -0
- annotuneagent/annotate.py +93 -0
- annotuneagent/arbitrate.py +141 -0
- annotuneagent/cli.py +366 -0
- annotuneagent/decide.py +725 -0
- annotuneagent/evaluate.py +121 -0
- annotuneagent/evidence.py +606 -0
- annotuneagent/granularity/__init__.py +28 -0
- annotuneagent/granularity/cl_dag.py +179 -0
- annotuneagent/granularity/ontology_map.py +108 -0
- annotuneagent/granularity/relations.py +96 -0
- annotuneagent/hypotheses.py +38 -0
- annotuneagent/local_lora.py +706 -0
- annotuneagent/marker_plot.py +172 -0
- annotuneagent/markers.py +206 -0
- annotuneagent/markers.tsv +378 -0
- annotuneagent/mltools.py +128 -0
- annotuneagent/ontology/cl_isa.json +1 -0
- annotuneagent/ontology/granularity_policy.yaml +8 -0
- annotuneagent/ontology/ontology_map.jsonl +198 -0
- annotuneagent/prepare.py +1385 -0
- annotuneagent/py.typed +0 -0
- annotuneagent/read.py +76 -0
- annotuneagent/tokenizer/LICENSE +202 -0
- annotuneagent/tokenizer/merges.txt +247587 -0
- annotuneagent/tokenizer/tokenizer.json +495920 -0
- annotuneagent/tokenizer/tokenizer_config.json +305 -0
- annotuneagent/tokenizer/vocab.json +248046 -0
- annotuneagent/votes.py +83 -0
- annotuneagent/workflow.py +2037 -0
- annotuneagent-0.1.0.dist-info/METADATA +208 -0
- annotuneagent-0.1.0.dist-info/RECORD +38 -0
- annotuneagent-0.1.0.dist-info/WHEEL +5 -0
- annotuneagent-0.1.0.dist-info/entry_points.txt +2 -0
- annotuneagent-0.1.0.dist-info/licenses/LICENSE +201 -0
- annotuneagent-0.1.0.dist-info/top_level.txt +1 -0
annotuneagent/mltools.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright 2026 Zhao Li (李钊)
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Tool-layer models. Negative-marker penalties stay a hard gate."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from .arbitrate import ScoredHypothesis
|
|
14
|
+
|
|
15
|
+
# Larger means a worse match. A penalized hypothesis is kept out of the set.
|
|
16
|
+
_PENALTY_NONCONFORMITY = 1.0e6
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Ranker:
|
|
21
|
+
"""Logistic scores for hypotheses that already passed the negative-marker gate."""
|
|
22
|
+
|
|
23
|
+
mean: tuple[float, ...]
|
|
24
|
+
scale: tuple[float, ...]
|
|
25
|
+
coef: tuple[float, ...]
|
|
26
|
+
intercept: float
|
|
27
|
+
|
|
28
|
+
def decision(self, values: np.ndarray) -> float:
|
|
29
|
+
scale = np.asarray(self.scale, dtype=float)
|
|
30
|
+
safe = np.where(scale == 0, 1.0, scale)
|
|
31
|
+
standardized = (np.asarray(values, dtype=float) - np.asarray(self.mean)) / safe
|
|
32
|
+
return float(standardized @ np.asarray(self.coef) + self.intercept)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def hypothesis_features(item: ScoredHypothesis) -> np.ndarray:
|
|
36
|
+
"""Mean supporting log2 fold-change, mean percent difference, and support fraction."""
|
|
37
|
+
supported = [gene for gene in item.positive if gene.logfc > 0]
|
|
38
|
+
if not supported:
|
|
39
|
+
return np.zeros(3, dtype=float)
|
|
40
|
+
logfc = float(np.mean([gene.logfc for gene in supported]))
|
|
41
|
+
delta = float(np.mean([gene.pct_in - gene.pct_out for gene in supported]))
|
|
42
|
+
fraction = len(supported) / len(item.positive)
|
|
43
|
+
return np.asarray([logfc, delta, fraction], dtype=float)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def fit_ranker(features: list[np.ndarray], labels: list[int]) -> Ranker:
|
|
47
|
+
"""Fit a logistic regression. ``labels`` are 1 when the hypothesis is correct."""
|
|
48
|
+
if len(features) != len(labels) or len(features) < 2:
|
|
49
|
+
raise ValueError("need at least two labeled feature rows")
|
|
50
|
+
if len(set(labels)) < 2:
|
|
51
|
+
raise ValueError("ranker labels must include both classes")
|
|
52
|
+
matrix = np.vstack(features)
|
|
53
|
+
mean = matrix.mean(axis=0)
|
|
54
|
+
scale = matrix.std(axis=0)
|
|
55
|
+
scale = np.where(scale == 0, 1.0, scale)
|
|
56
|
+
standardized = (matrix - mean) / scale
|
|
57
|
+
from sklearn.linear_model import LogisticRegression
|
|
58
|
+
|
|
59
|
+
model = LogisticRegression()
|
|
60
|
+
model.fit(standardized, np.asarray(labels, dtype=int))
|
|
61
|
+
return Ranker(
|
|
62
|
+
mean=tuple(float(value) for value in mean),
|
|
63
|
+
scale=tuple(float(value) for value in scale),
|
|
64
|
+
coef=tuple(float(value) for value in model.coef_.ravel()),
|
|
65
|
+
intercept=float(model.intercept_[0]),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def apply_ranker(scored: list[ScoredHypothesis], ranker: Ranker) -> list[ScoredHypothesis]:
|
|
70
|
+
"""Put every score on the ranker scale. A penalized hypothesis ranks below all others.
|
|
71
|
+
|
|
72
|
+
Penalized hypotheses keep their order among themselves. Mixing their
|
|
73
|
+
log fold-change scores with ranker scores would make margins meaningless.
|
|
74
|
+
"""
|
|
75
|
+
fitted = {
|
|
76
|
+
item.hypothesis.name: ranker.decision(hypothesis_features(item))
|
|
77
|
+
for item in scored
|
|
78
|
+
if item.penalty <= 0
|
|
79
|
+
}
|
|
80
|
+
floor = min(fitted.values(), default=0.0)
|
|
81
|
+
return [
|
|
82
|
+
replace(item, score=fitted[item.hypothesis.name])
|
|
83
|
+
if item.penalty <= 0
|
|
84
|
+
else replace(item, score=floor - item.penalty)
|
|
85
|
+
for item in scored
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def nonconformity(item: ScoredHypothesis, best_other: float) -> float:
|
|
90
|
+
if item.penalty > 0:
|
|
91
|
+
return _PENALTY_NONCONFORMITY
|
|
92
|
+
return float(best_other - item.score)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def calibration_nonconformity(scored: list[ScoredHypothesis], true_name: str) -> float:
|
|
96
|
+
"""Nonconformity of the author type on one calibration cluster."""
|
|
97
|
+
match = next((item for item in scored if item.hypothesis.name == true_name), None)
|
|
98
|
+
if match is None:
|
|
99
|
+
raise KeyError(true_name)
|
|
100
|
+
others = [item.score for item in scored if item.hypothesis.name != true_name]
|
|
101
|
+
best_other = max(others) if others else match.score
|
|
102
|
+
return nonconformity(match, best_other)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def conformal_quantile(values: list[float], alpha: float = 0.1) -> float:
|
|
106
|
+
"""Upper quantile for a split-conformal prediction set."""
|
|
107
|
+
if not values:
|
|
108
|
+
raise ValueError("calibration nonconformity is empty")
|
|
109
|
+
if not 0 < alpha < 1:
|
|
110
|
+
raise ValueError("alpha must be between 0 and 1")
|
|
111
|
+
ordered = sorted(float(value) for value in values)
|
|
112
|
+
rank = math.ceil((len(ordered) + 1) * (1 - alpha))
|
|
113
|
+
if rank > len(ordered):
|
|
114
|
+
return math.inf
|
|
115
|
+
return ordered[rank - 1]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def prediction_set(scored: list[ScoredHypothesis], quantile: float) -> list[str]:
|
|
119
|
+
"""Names whose nonconformity is at most ``quantile``. Penalized names stay out."""
|
|
120
|
+
names: list[str] = []
|
|
121
|
+
for item in scored:
|
|
122
|
+
if item.penalty > 0:
|
|
123
|
+
continue
|
|
124
|
+
others = [other.score for other in scored if other is not item]
|
|
125
|
+
best_other = max(others) if others else item.score
|
|
126
|
+
if nonconformity(item, best_other) <= quantile:
|
|
127
|
+
names.append(item.hypothesis.name)
|
|
128
|
+
return names
|