typevet 0.1.0.dev1__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.
- typevet/__init__.py +67 -0
- typevet/_version.py +43 -0
- typevet/adapters/__init__.py +16 -0
- typevet/adapters/diagnostics/__init__.py +59 -0
- typevet/adapters/diagnostics/fields.py +108 -0
- typevet/adapters/diagnostics/generation_events.py +75 -0
- typevet/adapters/diagnostics/http_events.py +91 -0
- typevet/adapters/diagnostics/logs.py +116 -0
- typevet/adapters/diagnostics/redaction.py +94 -0
- typevet/adapters/diagnostics/settings.py +77 -0
- typevet/adapters/inbound/__init__.py +65 -0
- typevet/adapters/inbound/api.py +61 -0
- typevet/adapters/inbound/backend_settings.py +608 -0
- typevet/adapters/inbound/cord_semantic_acceptance_cli.py +135 -0
- typevet/adapters/inbound/helpers.py +67 -0
- typevet/adapters/inbound/settings.py +154 -0
- typevet/adapters/outbound/__init__.py +62 -0
- typevet/adapters/outbound/async_fake.py +123 -0
- typevet/adapters/outbound/async_llama_cpp.py +168 -0
- typevet/adapters/outbound/chat_completion.py +148 -0
- typevet/adapters/outbound/fake.py +119 -0
- typevet/adapters/outbound/gemma/__init__.py +84 -0
- typevet/adapters/outbound/gemma/answer_binding.py +290 -0
- typevet/adapters/outbound/gemma/scoring_prefix.py +102 -0
- typevet/adapters/outbound/gemma/served_template.py +131 -0
- typevet/adapters/outbound/gemma_native_vision_factory.py +286 -0
- typevet/adapters/outbound/generation_finite.py +50 -0
- typevet/adapters/outbound/http_errors.py +38 -0
- typevet/adapters/outbound/judgment_scoring.py +471 -0
- typevet/adapters/outbound/llama_cpp.py +187 -0
- typevet/adapters/outbound/llama_cpp_http.py +103 -0
- typevet/adapters/outbound/llama_cpp_multimodal.py +145 -0
- typevet/adapters/outbound/llama_cpp_scoring.py +333 -0
- typevet/adapters/outbound/vllm_content.py +63 -0
- typevet/adapters/outbound/vllm_generation.py +163 -0
- typevet/adapters/outbound/vllm_generation_async.py +257 -0
- typevet/adapters/outbound/vllm_http.py +121 -0
- typevet/adapters/outbound/vllm_judgment_factory.py +210 -0
- typevet/adapters/outbound/vllm_scoring.py +372 -0
- typevet/domain/__init__.py +193 -0
- typevet/domain/candidate_scoring_request.py +134 -0
- typevet/domain/candidate_scoring_response.py +96 -0
- typevet/domain/candidate_scoring_validate.py +197 -0
- typevet/domain/decision_compile.py +368 -0
- typevet/domain/decision_execute.py +229 -0
- typevet/domain/decisions.py +129 -0
- typevet/domain/errors.py +235 -0
- typevet/domain/field_instructions.py +134 -0
- typevet/domain/judgment_answers.py +198 -0
- typevet/domain/judgment_normalize.py +243 -0
- typevet/domain/judgment_questions.py +187 -0
- typevet/domain/judgment_response.py +102 -0
- typevet/domain/media.py +101 -0
- typevet/domain/models.py +115 -0
- typevet/domain/question_schema.py +254 -0
- typevet/domain/scoring_stage.py +34 -0
- typevet/evaluation/__init__.py +19 -0
- typevet/evaluation/consumer_http_accounting.py +135 -0
- typevet/evaluation/cord_expense_call_accounting.py +108 -0
- typevet/evaluation/cord_expense_live_harness.py +75 -0
- typevet/evaluation/cord_expense_receipt_requirement.py +212 -0
- typevet/evaluation/cord_expense_smoke.py +328 -0
- typevet/evaluation/cord_semantic_acceptance.py +463 -0
- typevet/evaluation/cord_semantic_acceptance_report.py +90 -0
- typevet/evaluation/cord_semantic_metrics.py +57 -0
- typevet/evaluation/datasets/__init__.py +30 -0
- typevet/evaluation/datasets/banking77.py +270 -0
- typevet/evaluation/datasets/boolq.py +550 -0
- typevet/evaluation/datasets/boolq_download.py +132 -0
- typevet/evaluation/datasets/civil_comments.py +450 -0
- typevet/evaluation/datasets/clinc.py +125 -0
- typevet/evaluation/datasets/clinc_domains.json +172 -0
- typevet/evaluation/datasets/clinc_download.py +103 -0
- typevet/evaluation/datasets/clinc_plus_intent_names.json +153 -0
- typevet/evaluation/datasets/clinc_rows.py +306 -0
- typevet/evaluation/datasets/clinc_shard.py +156 -0
- typevet/evaluation/datasets/cord_expense.py +381 -0
- typevet/evaluation/datasets/difraud.py +256 -0
- typevet/evaluation/datasets/go_emotions.py +368 -0
- typevet/evaluation/datasets/go_emotions_download.py +154 -0
- typevet/evaluation/datasets/hyperpartisan.py +562 -0
- typevet/evaluation/datasets/partner_guard.py +170 -0
- typevet/evaluation/datasets/psai.py +415 -0
- typevet/evaluation/datasets/psai_download.py +167 -0
- typevet/evaluation/datasets/psai_evidence_pilot.py +570 -0
- typevet/evaluation/datasets/psai_schema.py +157 -0
- typevet/evaluation/datasets/psai_stream.py +166 -0
- typevet/evaluation/datasets/psai_vision.py +534 -0
- typevet/evaluation/datasets/psai_vision_controls.py +502 -0
- typevet/evaluation/datasets/pubmedqa.py +420 -0
- typevet/evaluation/experiment_identity.py +677 -0
- typevet/evaluation/psai_vision_consumer_accounting.py +190 -0
- typevet/evaluation/psai_vision_consumer_dispatch.py +216 -0
- typevet/evaluation/psai_vision_consumer_harness.py +342 -0
- typevet/evaluation/psai_vision_consumer_live.py +332 -0
- typevet/evaluation/psai_vision_consumer_live_identity.py +156 -0
- typevet/evaluation/psai_vision_consumer_live_receipt.py +132 -0
- typevet/evaluation/psai_vision_consumer_live_router.py +162 -0
- typevet/evaluation/psai_vision_consumer_offline.py +418 -0
- typevet/evaluation/psai_vision_consumer_outcomes.py +224 -0
- typevet/evaluation/psai_vision_consumer_protocol.py +69 -0
- typevet/evaluation/psai_vision_consumer_receipt.py +161 -0
- typevet/evaluation/psai_vision_consumer_receipt_structure.py +263 -0
- typevet/evaluation/psai_vision_probability_evidence.py +336 -0
- typevet/evaluation/runner/__init__.py +56 -0
- typevet/evaluation/runner/core.py +104 -0
- typevet/evaluation/runner/datasets.py +186 -0
- typevet/evaluation/runner/live_gate.py +149 -0
- typevet/evaluation/runner/report.py +116 -0
- typevet/evaluation/tpjep/__init__.py +81 -0
- typevet/evaluation/tpjep/loader.py +237 -0
- typevet/evaluation/tpjep/outcome.py +101 -0
- typevet/evaluation/tpjep/records.py +428 -0
- typevet/evaluation/tpjep/runner.py +325 -0
- typevet/ports/__init__.py +39 -0
- typevet/ports/async_generation.py +57 -0
- typevet/ports/framing.py +59 -0
- typevet/ports/generation.py +58 -0
- typevet/ports/judgment.py +71 -0
- typevet/ports/scoring.py +64 -0
- typevet/py.typed +0 -0
- typevet/runtime/__init__.py +50 -0
- typevet/runtime/categorical.py +196 -0
- typevet/runtime/judgment.py +17 -0
- typevet/runtime/llama_cpp_gemma_vision.py +28 -0
- typevet/runtime/scoring_prefix.py +14 -0
- typevet/runtime/vllm_judgment.py +28 -0
- typevet/testing/__init__.py +22 -0
- typevet/testing/fakes.py +152 -0
- typevet-0.1.0.dev1.dist-info/METADATA +125 -0
- typevet-0.1.0.dev1.dist-info/RECORD +133 -0
- typevet-0.1.0.dev1.dist-info/WHEEL +4 -0
- typevet-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Questions, states and image controls for the PSAI vision smoke ([#154][i154]).
|
|
2
|
+
|
|
3
|
+
Two legs share the fixture set.
|
|
4
|
+
|
|
5
|
+
The **annotation leg** asks ``category`` and ``requires_login`` over the PSAI
|
|
6
|
+
``task_name`` state, exactly as the metadata loader does. Hub annotations back
|
|
7
|
+
both answers.
|
|
8
|
+
|
|
9
|
+
The **visual leg** asks the manual ``shows_fox_news_chrome`` Noul three ways for
|
|
10
|
+
every row: image present, image omitted, and image swapped with the opposite
|
|
11
|
+
visual family. It must not reuse ``task_name``. Every PSAI task text pins its
|
|
12
|
+
site (``Only use http://foxnews.com``), which hands the model the answer as
|
|
13
|
+
text and would void the controls. ``VISUAL_CONTROL_STATE`` is family-neutral
|
|
14
|
+
instead, so pixels are the only family signal in the prompt.
|
|
15
|
+
|
|
16
|
+
An omitted image is **never** a semantic hit. With no image the model still
|
|
17
|
+
answers from its prior, so a match there measures the prior, not the pixels.
|
|
18
|
+
|
|
19
|
+
Two rules read the visual leg, and they differ in what they assume.
|
|
20
|
+
``semantic_hit`` reads one probability against ``NOUL_THRESHOLD``, which only
|
|
21
|
+
means something when the Noul is calibrated. ``paired_image_ordering`` compares
|
|
22
|
+
the two imaged controls of one row against each other. Their text is identical,
|
|
23
|
+
so the difference is the pixels, and no calibration assumption is needed. Prefer
|
|
24
|
+
the paired rule as the gate and keep the threshold reading as a diagnostic.
|
|
25
|
+
|
|
26
|
+
[i154]: https://github.com/Alberto-Codes/typevet/issues/154
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
Build the control matrix for the vendored rows:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from typevet.evaluation.datasets.psai_vision import load_vision_smoke
|
|
35
|
+
from typevet.evaluation.datasets.psai_vision_controls import control_matrix
|
|
36
|
+
|
|
37
|
+
root = Path("tests/fixtures/psai/vision_smoke")
|
|
38
|
+
fixture = load_vision_smoke((root / "manifest.json").read_text())
|
|
39
|
+
controls = control_matrix(fixture.examples)
|
|
40
|
+
assert len(controls) == len(fixture.examples) * 3
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See Also:
|
|
44
|
+
- [typevet.evaluation.datasets.psai_vision][]: fixture loading and provenance
|
|
45
|
+
- [typevet.ports.judgment][]: the ``judge`` surface these questions feed
|
|
46
|
+
- docs/how-to/run-the-psai-vision-smoke.md: the recipe
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
VISUAL_CONTROL_STATE (str): Family-neutral state for the visual leg.
|
|
50
|
+
CONDITION_PRESENT (str): Correct screenshot attached.
|
|
51
|
+
CONDITION_OMITTED (str): Same text, no image.
|
|
52
|
+
CONDITION_SWAPPED (str): Same text, opposite-family screenshot.
|
|
53
|
+
CONDITIONS (tuple[str, ...]): The three conditions in run order.
|
|
54
|
+
FAMILY_LEAK_TERMS (tuple[str, ...]): Terms a visual state must never hold.
|
|
55
|
+
NOUL_THRESHOLD (float): Probability at or above which a Noul reads true.
|
|
56
|
+
PAIRED_MARGIN_FLOOR (float): Smallest present-against-swapped margin that
|
|
57
|
+
still counts as image-conditioned.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
from collections.abc import Mapping, Sequence
|
|
63
|
+
from dataclasses import dataclass
|
|
64
|
+
from typing import Final
|
|
65
|
+
|
|
66
|
+
from typevet.domain.judgment_questions import Choice, Noul, Question
|
|
67
|
+
from typevet.evaluation.datasets.psai import psai_state, validate_task_state
|
|
68
|
+
from typevet.evaluation.datasets.psai_schema import (
|
|
69
|
+
CATEGORY_LABELS,
|
|
70
|
+
METADATA_DECISIONS_SCHEMA,
|
|
71
|
+
PRIMARY_NOUL_NAME,
|
|
72
|
+
)
|
|
73
|
+
from typevet.evaluation.datasets.psai_vision import (
|
|
74
|
+
ANNOTATION_PROVENANCE,
|
|
75
|
+
FOX_FAMILY,
|
|
76
|
+
MANUAL_VISUAL_PROVENANCE,
|
|
77
|
+
NON_FOX_FAMILY,
|
|
78
|
+
VISUAL_FAMILIES,
|
|
79
|
+
VISUAL_QUESTION_NAME,
|
|
80
|
+
PsaiVisionExample,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
VISUAL_CONTROL_STATE: Final[str] = (
|
|
84
|
+
"A screen capture taken during a recorded desktop browsing session."
|
|
85
|
+
)
|
|
86
|
+
CONDITION_PRESENT: Final[str] = "present"
|
|
87
|
+
CONDITION_OMITTED: Final[str] = "omitted"
|
|
88
|
+
CONDITION_SWAPPED: Final[str] = "swapped"
|
|
89
|
+
CONDITIONS: Final[tuple[str, ...]] = (
|
|
90
|
+
CONDITION_PRESENT,
|
|
91
|
+
CONDITION_OMITTED,
|
|
92
|
+
CONDITION_SWAPPED,
|
|
93
|
+
)
|
|
94
|
+
FAMILY_LEAK_TERMS: Final[tuple[str, ...]] = (
|
|
95
|
+
"fox",
|
|
96
|
+
"news",
|
|
97
|
+
"home depot",
|
|
98
|
+
"homedepot",
|
|
99
|
+
"squarespace",
|
|
100
|
+
)
|
|
101
|
+
NOUL_THRESHOLD: Final[float] = 0.5
|
|
102
|
+
PAIRED_MARGIN_FLOOR: Final[float] = 0.10
|
|
103
|
+
|
|
104
|
+
_VISUAL_INSTRUCTIONS: Final[str] = "Is the website in this screenshot Fox News?"
|
|
105
|
+
_VISUAL_CRITERIA: Final[dict[str, str]] = {
|
|
106
|
+
"true": ("Fox News site chrome or branding is clearly visible in the screenshot."),
|
|
107
|
+
"false": "It is not.",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class VisualControl:
|
|
113
|
+
"""One matched condition for the visually dependent question.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
unique_data_id (str): Row under test.
|
|
117
|
+
visual_family (str): Visual family of that row.
|
|
118
|
+
condition (str): One of ``CONDITIONS``.
|
|
119
|
+
state (str): Prompt state; always the family-neutral constant.
|
|
120
|
+
image_file (str | None): Screenshot to attach, ``None`` when omitted.
|
|
121
|
+
image_unique_data_id (str | None): Row the screenshot came from.
|
|
122
|
+
expected (bool | None): Gold polarity, ``None`` when nothing is provable.
|
|
123
|
+
counts_as_semantic_hit (bool): Whether a match may score.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
```python
|
|
127
|
+
from typevet.evaluation.datasets.psai_vision_controls import VisualControl
|
|
128
|
+
|
|
129
|
+
control = VisualControl(
|
|
130
|
+
unique_data_id="row",
|
|
131
|
+
visual_family="fox_news",
|
|
132
|
+
condition="omitted",
|
|
133
|
+
state="neutral",
|
|
134
|
+
image_file=None,
|
|
135
|
+
image_unique_data_id=None,
|
|
136
|
+
expected=None,
|
|
137
|
+
counts_as_semantic_hit=False,
|
|
138
|
+
)
|
|
139
|
+
assert control.expected is None
|
|
140
|
+
```
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
unique_data_id: str
|
|
144
|
+
visual_family: str
|
|
145
|
+
condition: str
|
|
146
|
+
state: str
|
|
147
|
+
image_file: str | None
|
|
148
|
+
image_unique_data_id: str | None
|
|
149
|
+
expected: bool | None
|
|
150
|
+
counts_as_semantic_hit: bool
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def visual_question() -> Noul:
|
|
154
|
+
"""Return the manual visually dependent Noul.
|
|
155
|
+
|
|
156
|
+
The text names Fox News because that is the question. It never says which
|
|
157
|
+
row or family the attached screenshot belongs to.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Noul with ``true`` and ``false`` criteria.
|
|
161
|
+
"""
|
|
162
|
+
return Noul(instructions=_VISUAL_INSTRUCTIONS, criteria=dict(_VISUAL_CRITERIA))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def annotation_questions() -> dict[str, Question]:
|
|
166
|
+
"""Return the Hub annotation-backed questions.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
``category`` Choice and ``requires_login`` Noul, with Hub instructions.
|
|
170
|
+
"""
|
|
171
|
+
props = METADATA_DECISIONS_SCHEMA["properties"]
|
|
172
|
+
return {
|
|
173
|
+
"category": Choice(
|
|
174
|
+
criteria=dict.fromkeys(CATEGORY_LABELS, None),
|
|
175
|
+
instructions=props["category"]["instructions"],
|
|
176
|
+
),
|
|
177
|
+
PRIMARY_NOUL_NAME: Noul(instructions=props[PRIMARY_NOUL_NAME]["instructions"]),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def vision_questions() -> dict[str, Question]:
|
|
182
|
+
"""Return every question the smoke scores.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The annotation-backed pair plus the manual visual Noul.
|
|
186
|
+
"""
|
|
187
|
+
questions = annotation_questions()
|
|
188
|
+
questions[VISUAL_QUESTION_NAME] = visual_question()
|
|
189
|
+
return questions
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def question_provenance() -> dict[str, str]:
|
|
193
|
+
"""Return the gold provenance of each question.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Question name to ``annotation`` or ``manual_visual``.
|
|
197
|
+
"""
|
|
198
|
+
return {
|
|
199
|
+
"category": ANNOTATION_PROVENANCE,
|
|
200
|
+
PRIMARY_NOUL_NAME: ANNOTATION_PROVENANCE,
|
|
201
|
+
VISUAL_QUESTION_NAME: MANUAL_VISUAL_PROVENANCE,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def annotation_state(example: PsaiVisionExample) -> dict[str, str]:
|
|
206
|
+
"""Build the annotation-leg state for one row.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
example: Row whose task text to evaluate.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Mapping with a single ``task_name`` key.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
ValueError: When the state would carry a banned leakage key.
|
|
216
|
+
"""
|
|
217
|
+
state = psai_state(example.task_name)
|
|
218
|
+
validate_task_state(state)
|
|
219
|
+
return state
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def validate_no_family_leak(text: str) -> None:
|
|
223
|
+
"""Reject prompt text that names a visual family.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
text: Candidate state text for the visual leg.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ValueError: When ``text`` holds a term from ``FAMILY_LEAK_TERMS``, which
|
|
230
|
+
would let the model answer the visual question without the image.
|
|
231
|
+
"""
|
|
232
|
+
lowered = text.lower()
|
|
233
|
+
found = [term for term in FAMILY_LEAK_TERMS if term in lowered]
|
|
234
|
+
if found:
|
|
235
|
+
names = ", ".join(found)
|
|
236
|
+
msg = (
|
|
237
|
+
f"visual-leg text would leak the visual family; remove: {names}. "
|
|
238
|
+
"A text-derivable answer voids the image controls."
|
|
239
|
+
)
|
|
240
|
+
raise ValueError(msg)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _swap_donors(
|
|
244
|
+
examples: Sequence[PsaiVisionExample],
|
|
245
|
+
) -> dict[str, PsaiVisionExample]:
|
|
246
|
+
"""Pair each row with a deterministic opposite-family donor.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
examples: Rows to pair.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
``unique_data_id`` to the opposite-family row that donates its image.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
ValueError: When either visual family has no row to donate.
|
|
256
|
+
"""
|
|
257
|
+
by_family = {
|
|
258
|
+
family: sorted(
|
|
259
|
+
(e for e in examples if e.visual_family == family),
|
|
260
|
+
key=lambda e: e.stream_order,
|
|
261
|
+
)
|
|
262
|
+
for family in VISUAL_FAMILIES
|
|
263
|
+
}
|
|
264
|
+
for family, rows in by_family.items():
|
|
265
|
+
if not rows:
|
|
266
|
+
msg = (
|
|
267
|
+
f"the swap control needs at least one {family!r} row; "
|
|
268
|
+
f"got none among {len(examples)} rows"
|
|
269
|
+
)
|
|
270
|
+
raise ValueError(msg)
|
|
271
|
+
opposite = {FOX_FAMILY: NON_FOX_FAMILY, NON_FOX_FAMILY: FOX_FAMILY}
|
|
272
|
+
donors: dict[str, PsaiVisionExample] = {}
|
|
273
|
+
for family, rows in by_family.items():
|
|
274
|
+
pool = by_family[opposite[family]]
|
|
275
|
+
for position, row in enumerate(rows):
|
|
276
|
+
donors[row.unique_data_id] = pool[position % len(pool)]
|
|
277
|
+
return donors
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def control_matrix(
|
|
281
|
+
examples: Sequence[PsaiVisionExample],
|
|
282
|
+
) -> tuple[VisualControl, ...]:
|
|
283
|
+
"""Build matched present / omitted / swapped controls for every row.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
examples: Rows to evaluate; both visual families must appear.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Controls grouped by row, in ``CONDITIONS`` order.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
ValueError: When either visual family has no row, so no swap donor
|
|
293
|
+
exists.
|
|
294
|
+
"""
|
|
295
|
+
donors = _swap_donors(examples)
|
|
296
|
+
controls: list[VisualControl] = []
|
|
297
|
+
for example in examples:
|
|
298
|
+
donor = donors[example.unique_data_id]
|
|
299
|
+
controls.extend(
|
|
300
|
+
(
|
|
301
|
+
VisualControl(
|
|
302
|
+
unique_data_id=example.unique_data_id,
|
|
303
|
+
visual_family=example.visual_family,
|
|
304
|
+
condition=CONDITION_PRESENT,
|
|
305
|
+
state=VISUAL_CONTROL_STATE,
|
|
306
|
+
image_file=example.screenshot.file_name,
|
|
307
|
+
image_unique_data_id=example.unique_data_id,
|
|
308
|
+
expected=example.shows_fox_news_chrome,
|
|
309
|
+
counts_as_semantic_hit=True,
|
|
310
|
+
),
|
|
311
|
+
VisualControl(
|
|
312
|
+
unique_data_id=example.unique_data_id,
|
|
313
|
+
visual_family=example.visual_family,
|
|
314
|
+
condition=CONDITION_OMITTED,
|
|
315
|
+
state=VISUAL_CONTROL_STATE,
|
|
316
|
+
image_file=None,
|
|
317
|
+
image_unique_data_id=None,
|
|
318
|
+
expected=None,
|
|
319
|
+
counts_as_semantic_hit=False,
|
|
320
|
+
),
|
|
321
|
+
VisualControl(
|
|
322
|
+
unique_data_id=example.unique_data_id,
|
|
323
|
+
visual_family=example.visual_family,
|
|
324
|
+
condition=CONDITION_SWAPPED,
|
|
325
|
+
state=VISUAL_CONTROL_STATE,
|
|
326
|
+
image_file=donor.screenshot.file_name,
|
|
327
|
+
image_unique_data_id=donor.unique_data_id,
|
|
328
|
+
expected=donor.shows_fox_news_chrome,
|
|
329
|
+
counts_as_semantic_hit=True,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
return tuple(controls)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def noul_polarity(probability: float, threshold: float = NOUL_THRESHOLD) -> bool:
|
|
337
|
+
"""Read a Noul probability as a boolean answer.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
probability: Probability the scorer gave the ``true`` outcome.
|
|
341
|
+
threshold: Cut at or above which the answer reads true.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
``True`` when ``probability >= threshold``.
|
|
345
|
+
"""
|
|
346
|
+
return probability >= threshold
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def semantic_hit(
|
|
350
|
+
control: VisualControl,
|
|
351
|
+
probability: float,
|
|
352
|
+
threshold: float = NOUL_THRESHOLD,
|
|
353
|
+
) -> bool:
|
|
354
|
+
"""Score one control against the contract semantic rule.
|
|
355
|
+
|
|
356
|
+
Present must match the row's own gold. Swapped must match the donor family,
|
|
357
|
+
so the polarity flips. Omitted never scores, whatever it answered.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
control: Control row under test.
|
|
361
|
+
probability: Scored probability of the ``true`` outcome.
|
|
362
|
+
threshold: Polarity cut passed to ``noul_polarity``.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
``True`` only for a counted control whose polarity matches ``expected``.
|
|
366
|
+
"""
|
|
367
|
+
if not control.counts_as_semantic_hit or control.expected is None:
|
|
368
|
+
return False
|
|
369
|
+
return noul_polarity(probability, threshold) is control.expected
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@dataclass(frozen=True, slots=True)
|
|
373
|
+
class PairedOrdering:
|
|
374
|
+
"""The present/swapped pair for one row, compared image against image.
|
|
375
|
+
|
|
376
|
+
Both members carry byte-identical text, so the probability difference is
|
|
377
|
+
attributable to the pixels alone. This needs no calibration assumption,
|
|
378
|
+
unlike reading one probability against a fixed threshold.
|
|
379
|
+
|
|
380
|
+
Attributes:
|
|
381
|
+
unique_data_id (str): Row the pair belongs to.
|
|
382
|
+
fox_image_id (str): Row that supplied the Fox News screenshot.
|
|
383
|
+
fox_probability (float): Scored ``true`` probability for that image.
|
|
384
|
+
non_fox_image_id (str): Row that supplied the non-Fox screenshot.
|
|
385
|
+
non_fox_probability (float): Scored ``true`` probability for that image.
|
|
386
|
+
margin (float): ``fox_probability - non_fox_probability``.
|
|
387
|
+
ordered (bool): Whether ``margin`` clears the floor.
|
|
388
|
+
|
|
389
|
+
Examples:
|
|
390
|
+
```python
|
|
391
|
+
from typevet.evaluation.datasets.psai_vision_controls import PairedOrdering
|
|
392
|
+
|
|
393
|
+
pair = PairedOrdering(
|
|
394
|
+
unique_data_id="row",
|
|
395
|
+
fox_image_id="fox",
|
|
396
|
+
fox_probability=0.92,
|
|
397
|
+
non_fox_image_id="shop",
|
|
398
|
+
non_fox_probability=0.09,
|
|
399
|
+
margin=0.83,
|
|
400
|
+
ordered=True,
|
|
401
|
+
)
|
|
402
|
+
assert pair.ordered
|
|
403
|
+
```
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
unique_data_id: str
|
|
407
|
+
fox_image_id: str
|
|
408
|
+
fox_probability: float
|
|
409
|
+
non_fox_image_id: str
|
|
410
|
+
non_fox_probability: float
|
|
411
|
+
margin: float
|
|
412
|
+
ordered: bool
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def paired_image_ordering(
|
|
416
|
+
controls: Sequence[VisualControl],
|
|
417
|
+
probabilities: Mapping[tuple[str, str], float],
|
|
418
|
+
floor: float = PAIRED_MARGIN_FLOOR,
|
|
419
|
+
) -> tuple[PairedOrdering, ...]:
|
|
420
|
+
"""Compare each row's two imaged controls against one another.
|
|
421
|
+
|
|
422
|
+
The present and swapped controls of one row always hold opposite families,
|
|
423
|
+
because the swap donor comes from the other family. Requiring the Fox image
|
|
424
|
+
to outscore the non-Fox image tests conditioning without assuming the model
|
|
425
|
+
is calibrated at any particular cut.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
controls: Control rows that were run.
|
|
429
|
+
probabilities: ``(unique_data_id, condition)`` to scored probability.
|
|
430
|
+
floor: Smallest margin that still counts as ordered.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
One ``PairedOrdering`` per row, in row order.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
KeyError: When an imaged control has no scored probability.
|
|
437
|
+
ValueError: When a row does not hold one Fox and one non-Fox image.
|
|
438
|
+
"""
|
|
439
|
+
pairs: list[PairedOrdering] = []
|
|
440
|
+
for uid in dict.fromkeys(control.unique_data_id for control in controls):
|
|
441
|
+
imaged = [
|
|
442
|
+
control
|
|
443
|
+
for control in controls
|
|
444
|
+
if control.unique_data_id == uid and control.image_unique_data_id
|
|
445
|
+
]
|
|
446
|
+
fox = [control for control in imaged if control.expected is True]
|
|
447
|
+
non_fox = [control for control in imaged if control.expected is False]
|
|
448
|
+
if len(fox) != 1 or len(non_fox) != 1:
|
|
449
|
+
msg = (
|
|
450
|
+
f"row {uid!r} needs one Fox and one non-Fox image to pair; got "
|
|
451
|
+
f"{len(fox)} Fox and {len(non_fox)} non-Fox"
|
|
452
|
+
)
|
|
453
|
+
raise ValueError(msg)
|
|
454
|
+
fox_probability = probabilities[(uid, fox[0].condition)]
|
|
455
|
+
non_fox_probability = probabilities[(uid, non_fox[0].condition)]
|
|
456
|
+
margin = fox_probability - non_fox_probability
|
|
457
|
+
pairs.append(
|
|
458
|
+
PairedOrdering(
|
|
459
|
+
unique_data_id=uid,
|
|
460
|
+
fox_image_id=str(fox[0].image_unique_data_id),
|
|
461
|
+
fox_probability=fox_probability,
|
|
462
|
+
non_fox_image_id=str(non_fox[0].image_unique_data_id),
|
|
463
|
+
non_fox_probability=non_fox_probability,
|
|
464
|
+
margin=margin,
|
|
465
|
+
ordered=margin >= floor,
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
return tuple(pairs)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def control_summary(
|
|
472
|
+
controls: Sequence[VisualControl],
|
|
473
|
+
probabilities: Mapping[tuple[str, str], float],
|
|
474
|
+
threshold: float = NOUL_THRESHOLD,
|
|
475
|
+
) -> dict[str, int]:
|
|
476
|
+
"""Count counted controls and semantic hits for a receipt.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
controls: Control rows that were run.
|
|
480
|
+
probabilities: ``(unique_data_id, condition)`` to scored probability.
|
|
481
|
+
threshold: Polarity cut passed to ``semantic_hit``.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Counts under ``counted``, ``hits`` and ``uncounted``.
|
|
485
|
+
|
|
486
|
+
Raises:
|
|
487
|
+
KeyError: When a control has no scored probability.
|
|
488
|
+
"""
|
|
489
|
+
counted = 0
|
|
490
|
+
hits = 0
|
|
491
|
+
for control in controls:
|
|
492
|
+
probability = probabilities[(control.unique_data_id, control.condition)]
|
|
493
|
+
if not control.counts_as_semantic_hit:
|
|
494
|
+
continue
|
|
495
|
+
counted += 1
|
|
496
|
+
if semantic_hit(control, probability, threshold):
|
|
497
|
+
hits += 1
|
|
498
|
+
return {
|
|
499
|
+
"counted": counted,
|
|
500
|
+
"hits": hits,
|
|
501
|
+
"uncounted": len(controls) - counted,
|
|
502
|
+
}
|