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.
Files changed (38) hide show
  1. annotuneagent/__init__.py +68 -0
  2. annotuneagent/__main__.py +6 -0
  3. annotuneagent/_version.py +19 -0
  4. annotuneagent/annotate.py +93 -0
  5. annotuneagent/arbitrate.py +141 -0
  6. annotuneagent/cli.py +366 -0
  7. annotuneagent/decide.py +725 -0
  8. annotuneagent/evaluate.py +121 -0
  9. annotuneagent/evidence.py +606 -0
  10. annotuneagent/granularity/__init__.py +28 -0
  11. annotuneagent/granularity/cl_dag.py +179 -0
  12. annotuneagent/granularity/ontology_map.py +108 -0
  13. annotuneagent/granularity/relations.py +96 -0
  14. annotuneagent/hypotheses.py +38 -0
  15. annotuneagent/local_lora.py +706 -0
  16. annotuneagent/marker_plot.py +172 -0
  17. annotuneagent/markers.py +206 -0
  18. annotuneagent/markers.tsv +378 -0
  19. annotuneagent/mltools.py +128 -0
  20. annotuneagent/ontology/cl_isa.json +1 -0
  21. annotuneagent/ontology/granularity_policy.yaml +8 -0
  22. annotuneagent/ontology/ontology_map.jsonl +198 -0
  23. annotuneagent/prepare.py +1385 -0
  24. annotuneagent/py.typed +0 -0
  25. annotuneagent/read.py +76 -0
  26. annotuneagent/tokenizer/LICENSE +202 -0
  27. annotuneagent/tokenizer/merges.txt +247587 -0
  28. annotuneagent/tokenizer/tokenizer.json +495920 -0
  29. annotuneagent/tokenizer/tokenizer_config.json +305 -0
  30. annotuneagent/tokenizer/vocab.json +248046 -0
  31. annotuneagent/votes.py +83 -0
  32. annotuneagent/workflow.py +2037 -0
  33. annotuneagent-0.1.0.dist-info/METADATA +208 -0
  34. annotuneagent-0.1.0.dist-info/RECORD +38 -0
  35. annotuneagent-0.1.0.dist-info/WHEEL +5 -0
  36. annotuneagent-0.1.0.dist-info/entry_points.txt +2 -0
  37. annotuneagent-0.1.0.dist-info/licenses/LICENSE +201 -0
  38. annotuneagent-0.1.0.dist-info/top_level.txt +1 -0
@@ -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