modelspec-dev 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.
- api/__init__.py +0 -0
- api/class_fit.py +334 -0
- api/classes.py +557 -0
- api/ranking/__init__.py +12 -0
- api/ranking/engine.py +1943 -0
- cli/__init__.py +0 -0
- cli/modelspec/__init__.py +0 -0
- cli/modelspec/cli.py +1819 -0
- cli/modelspec/commands/__init__.py +0 -0
- cli/modelspec/decide_cmd.py +333 -0
- cli/modelspec/offline.py +623 -0
- cli/modelspec/snapshot.py +698 -0
- cli/modelspec/snapshot_build_cmd.py +49 -0
- cli/modelspec/verify_cmd.py +125 -0
- cli/modelspec/vocab_cmd.py +204 -0
- cli/modelspec/vocabulary_cache.py +54 -0
- decision/__init__.py +13 -0
- decision/capability.py +872 -0
- decision/computed.py +125 -0
- decision/contract.py +1575 -0
- decision/engine.py +238 -0
- decision/excluded.py +34 -0
- decision/explain.py +908 -0
- decision/filter.py +796 -0
- decision/model.py +438 -0
- decision/normalise.py +604 -0
- decision/optimise.py +320 -0
- decision/registry.py +717 -0
- decision/relax.py +132 -0
- decision/resolve.py +111 -0
- decision/schema.py +21 -0
- decision/snapshot.py +1483 -0
- decision/sources.py +544 -0
- decision/templates.py +134 -0
- decision/verify.py +1745 -0
- decision/vocabulary.py +433 -0
- modelspec_dev-0.1.0.dist-info/METADATA +101 -0
- modelspec_dev-0.1.0.dist-info/RECORD +63 -0
- modelspec_dev-0.1.0.dist-info/WHEEL +4 -0
- modelspec_dev-0.1.0.dist-info/entry_points.txt +2 -0
- modelspec_dev-0.1.0.dist-info/licenses/LICENSE +43 -0
- modelspec_dev-0.1.0.dist-info/licenses/LICENSE-DATA +428 -0
- pipeline/__init__.py +0 -0
- pipeline/class_export.py +172 -0
- pipeline/hardware.py +434 -0
- pipeline/hosts.py +247 -0
- pipeline/load.py +224 -0
- pipeline/ranking.py +551 -0
- registry/domains.yaml +130 -0
- registry/facets.yaml +888 -0
- registry/harnesses.yaml +79 -0
- registry/providers.yaml +354 -0
- registry/sources.yaml +3059 -0
- registry/templates.yaml +166 -0
- schema/__init__.py +0 -0
- schema/applicability.py +147 -0
- schema/benchmark.py +175 -0
- schema/benchmark_eligibility.py +304 -0
- schema/card.py +1463 -0
- schema/enrichment.py +162 -0
- schema/enums.py +327 -0
- schema/graph.py +406 -0
- schema/suppliers.py +72 -0
decision/filter.py
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
"""Three-valued filter (MODEL-141, design §6.2).
|
|
2
|
+
|
|
3
|
+
A condition is pass, fail or unknown for each candidate. ``all`` is Kleene AND:
|
|
4
|
+
a fail wins over an unknown. ``any`` is Kleene OR: a pass wins over an unknown.
|
|
5
|
+
``not`` swaps pass and fail and leaves unknown unknown.
|
|
6
|
+
|
|
7
|
+
The unknown policy comes from the condition, otherwise from the facets that
|
|
8
|
+
are unknown for that candidate. Capability moves the candidate to
|
|
9
|
+
``may_qualify``. A governance unknown does not pass; the elimination is
|
|
10
|
+
surfaced as ``unverified: may qualify``. A governance facet the candidate
|
|
11
|
+
already passes does not change this. A per-condition ``unknown`` override wins.
|
|
12
|
+
``soft`` does not change who remains. No condition adds a score.
|
|
13
|
+
|
|
14
|
+
``ids_where`` is called with ``= != < <= > >=`` and ``known``. A window is
|
|
15
|
+
``>=`` intersected with ``<=``. A set is an ``any`` of ``=``, or on a
|
|
16
|
+
set-valued facet one ``contains_any``. An evidence
|
|
17
|
+
condition calls ``evidence`` and then applies its qualifiers; ``@direct`` asks
|
|
18
|
+
whether the benchmark is direct for a capability the spec requests. Retired
|
|
19
|
+
candidates are excluded unless the resolved spec asks for lifecycle ``retired``.
|
|
20
|
+
|
|
21
|
+
A model with offerings is represented by them (MODEL-159). Its bare model row
|
|
22
|
+
would tie with them on the evidence they inherit, so it never enters the
|
|
23
|
+
lineup and is not reported as eliminated. A model with no offering (open
|
|
24
|
+
weights run on one's own hardware) is its own row.
|
|
25
|
+
|
|
26
|
+
The shared snapshot protocol lives in ``decision/snapshot.py`` (MODEL-138).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Callable, Sequence
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Any, Literal
|
|
34
|
+
|
|
35
|
+
from decision.contract import (
|
|
36
|
+
AllOf,
|
|
37
|
+
AnyOf,
|
|
38
|
+
Compare,
|
|
39
|
+
EvidenceQualifiers,
|
|
40
|
+
FunnelStep,
|
|
41
|
+
InSet,
|
|
42
|
+
Issue,
|
|
43
|
+
Known,
|
|
44
|
+
ModelRef,
|
|
45
|
+
NotOf,
|
|
46
|
+
SpecError,
|
|
47
|
+
Window,
|
|
48
|
+
render_condition,
|
|
49
|
+
)
|
|
50
|
+
from decision.resolve import Resolved
|
|
51
|
+
|
|
52
|
+
Leg = Literal["pass", "fail", "unknown"]
|
|
53
|
+
UNVERIFIED_MAY_QUALIFY = "unverified: may qualify"
|
|
54
|
+
|
|
55
|
+
# Measurers that are not the model's own lab or provider.
|
|
56
|
+
_INDEPENDENT = frozenset({
|
|
57
|
+
"benchmark_author", "independent", "independent_evaluator", "modelspec", "outcome_protocol",
|
|
58
|
+
})
|
|
59
|
+
_PROVIDER = frozenset({"provider_self_report"})
|
|
60
|
+
|
|
61
|
+
_RETIRED_CONDITION = "model.lifecycle not in {retired}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Missing:
|
|
65
|
+
"""The reference model has no known value, so the comparison is unknown."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_MISSING = _Missing()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class Bits:
|
|
73
|
+
"""Three disjoint bitsets over ``candidates()``. A hole is unknown."""
|
|
74
|
+
|
|
75
|
+
passing: int = 0
|
|
76
|
+
failing: int = 0
|
|
77
|
+
unknown: int = 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class FunnelCount:
|
|
82
|
+
_condition: Any
|
|
83
|
+
before: int
|
|
84
|
+
after: int
|
|
85
|
+
may_qualify: int
|
|
86
|
+
models_before: int
|
|
87
|
+
models_after: int
|
|
88
|
+
offerings_before: int
|
|
89
|
+
offerings_after: int
|
|
90
|
+
models_may_qualify: int
|
|
91
|
+
offerings_may_qualify: int
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def condition(self) -> str:
|
|
95
|
+
if isinstance(self._condition, str):
|
|
96
|
+
return self._condition
|
|
97
|
+
return render_condition(self._condition)
|
|
98
|
+
|
|
99
|
+
def as_contract(self) -> FunnelStep:
|
|
100
|
+
return FunnelStep(
|
|
101
|
+
condition=self.condition, before=self.before, after=self.after,
|
|
102
|
+
may_qualify=self.may_qualify,
|
|
103
|
+
models_before=self.models_before, models_after=self.models_after,
|
|
104
|
+
offerings_before=self.offerings_before, offerings_after=self.offerings_after,
|
|
105
|
+
models_may_qualify=self.models_may_qualify,
|
|
106
|
+
offerings_may_qualify=self.offerings_may_qualify,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class Elimination:
|
|
112
|
+
"""Why one candidate left the lineup. ``value`` is theirs, ``threshold`` is the bar."""
|
|
113
|
+
|
|
114
|
+
candidate: str
|
|
115
|
+
_condition: Any
|
|
116
|
+
value: Any
|
|
117
|
+
threshold: Any
|
|
118
|
+
facet: str | None
|
|
119
|
+
unverified: bool = False
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def condition(self) -> str:
|
|
123
|
+
if isinstance(self._condition, str):
|
|
124
|
+
return self._condition
|
|
125
|
+
return render_condition(self._condition)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def surface(self) -> str | None:
|
|
129
|
+
if self.unverified:
|
|
130
|
+
return UNVERIFIED_MAY_QUALIFY
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class MayQualify:
|
|
136
|
+
"""A candidate removed from the ranking because a capability value is unknown."""
|
|
137
|
+
|
|
138
|
+
candidate: str
|
|
139
|
+
unknown: tuple[str, ...]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class SoftPenalty:
|
|
144
|
+
"""A soft condition, for the optimiser. It did not remove anyone."""
|
|
145
|
+
|
|
146
|
+
condition: str
|
|
147
|
+
penalty: float
|
|
148
|
+
failing: tuple[str, ...]
|
|
149
|
+
unknown: tuple[str, ...]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class FilterResult:
|
|
154
|
+
"""Who remains after the hard conditions. There is no score here."""
|
|
155
|
+
|
|
156
|
+
snapshot_id: str
|
|
157
|
+
feasible: tuple[str, ...]
|
|
158
|
+
may_qualify: tuple[MayQualify, ...]
|
|
159
|
+
funnel: tuple[FunnelCount, ...]
|
|
160
|
+
eliminated: tuple[Elimination, ...]
|
|
161
|
+
penalties: tuple[SoftPenalty, ...]
|
|
162
|
+
deprecated: tuple[str, ...]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def tri_not(leg: Leg) -> Leg:
|
|
166
|
+
if leg == "pass":
|
|
167
|
+
return "fail"
|
|
168
|
+
if leg == "fail":
|
|
169
|
+
return "pass"
|
|
170
|
+
return "unknown"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def tri_and(left: Leg, right: Leg) -> Leg:
|
|
174
|
+
if left == "fail" or right == "fail":
|
|
175
|
+
return "fail"
|
|
176
|
+
if left == "unknown" or right == "unknown":
|
|
177
|
+
return "unknown"
|
|
178
|
+
return "pass"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def tri_or(left: Leg, right: Leg) -> Leg:
|
|
182
|
+
if left == "pass" or right == "pass":
|
|
183
|
+
return "pass"
|
|
184
|
+
if left == "unknown" or right == "unknown":
|
|
185
|
+
return "unknown"
|
|
186
|
+
return "fail"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def bit_not(bits: Bits, universe: int) -> Bits:
|
|
190
|
+
return Bits(bits.failing & universe, bits.passing & universe, bits.unknown & universe)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def bit_and(left: Bits, right: Bits, universe: int) -> Bits:
|
|
194
|
+
passing = left.passing & right.passing & universe
|
|
195
|
+
failing = (left.failing | right.failing) & universe
|
|
196
|
+
return Bits(passing, failing, universe & ~passing & ~failing)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def bit_or(left: Bits, right: Bits, universe: int) -> Bits:
|
|
200
|
+
passing = (left.passing | right.passing) & universe
|
|
201
|
+
failing = left.failing & right.failing & universe
|
|
202
|
+
return Bits(passing, failing, universe & ~passing & ~failing)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _as_bits(raw: Any, universe: int) -> Bits:
|
|
206
|
+
passing = int(raw.passing) & universe
|
|
207
|
+
failing = int(raw.failing) & universe
|
|
208
|
+
unknown = int(raw.unknown) & universe
|
|
209
|
+
if (passing & failing) or (passing & unknown) or (failing & unknown):
|
|
210
|
+
raise ValueError("ids_where returned overlapping bitsets")
|
|
211
|
+
unknown |= universe & ~passing & ~failing
|
|
212
|
+
return Bits(passing, failing, unknown)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _measurers(qualifier: str | None) -> frozenset[str] | None:
|
|
216
|
+
if qualifier is None or qualifier == "any":
|
|
217
|
+
return None
|
|
218
|
+
if qualifier == "independent":
|
|
219
|
+
return _INDEPENDENT
|
|
220
|
+
if qualifier == "provider_self_report":
|
|
221
|
+
return _PROVIDER
|
|
222
|
+
raise ValueError(f"unknown measured_by qualifier {qualifier!r}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _op(op: str, left: Any, right: Any) -> bool:
|
|
226
|
+
if op == "=":
|
|
227
|
+
return left == right
|
|
228
|
+
if op == "!=":
|
|
229
|
+
return left != right
|
|
230
|
+
if op == "<":
|
|
231
|
+
return left < right
|
|
232
|
+
if op == "<=":
|
|
233
|
+
return left <= right
|
|
234
|
+
if op == ">":
|
|
235
|
+
return left > right
|
|
236
|
+
if op == ">=":
|
|
237
|
+
return left >= right
|
|
238
|
+
raise ValueError(f"unsupported op {op!r}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _judge(values: Sequence[Any], pred: Callable[[Any], bool]) -> Leg:
|
|
242
|
+
if not values:
|
|
243
|
+
return "unknown"
|
|
244
|
+
saw_fail = False
|
|
245
|
+
saw_unknown = False
|
|
246
|
+
for value in values:
|
|
247
|
+
try:
|
|
248
|
+
ok = pred(value)
|
|
249
|
+
except TypeError:
|
|
250
|
+
saw_unknown = True
|
|
251
|
+
continue
|
|
252
|
+
if ok:
|
|
253
|
+
return "pass"
|
|
254
|
+
saw_fail = True
|
|
255
|
+
if saw_unknown:
|
|
256
|
+
return "unknown"
|
|
257
|
+
return "fail" if saw_fail else "unknown"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _shown(values: Sequence[Any], op: str | None) -> Any:
|
|
261
|
+
ordered = [value for value in values if not isinstance(value, bool)]
|
|
262
|
+
if not ordered:
|
|
263
|
+
return None
|
|
264
|
+
if op in ("<", "<="):
|
|
265
|
+
return min(ordered)
|
|
266
|
+
if op in (">", ">="):
|
|
267
|
+
return max(ordered)
|
|
268
|
+
if len(ordered) == 1:
|
|
269
|
+
return ordered[0]
|
|
270
|
+
return tuple(ordered)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _collapse(bits: Bits, policy: str, universe: int) -> Bits:
|
|
274
|
+
if policy == "pass":
|
|
275
|
+
return Bits((bits.passing | bits.unknown) & universe, bits.failing & universe, 0)
|
|
276
|
+
if policy == "fail":
|
|
277
|
+
return Bits(bits.passing & universe, (bits.failing | bits.unknown) & universe, 0)
|
|
278
|
+
return bits
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _leg_of(bits: Bits, bit: int) -> Leg:
|
|
282
|
+
if bits.passing & bit:
|
|
283
|
+
return "pass"
|
|
284
|
+
if bits.failing & bit:
|
|
285
|
+
return "fail"
|
|
286
|
+
return "unknown"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _children(cond: Any) -> tuple[Any, ...]:
|
|
290
|
+
if isinstance(cond, AnyOf):
|
|
291
|
+
return tuple(cond.any)
|
|
292
|
+
if isinstance(cond, AllOf):
|
|
293
|
+
return tuple(cond.all)
|
|
294
|
+
if isinstance(cond, NotOf):
|
|
295
|
+
return (cond.not_,)
|
|
296
|
+
return ()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _facet_of(cond: Any) -> str | None:
|
|
300
|
+
if isinstance(cond, Known):
|
|
301
|
+
return cond.known
|
|
302
|
+
facet = getattr(cond, "facet", None)
|
|
303
|
+
return facet if isinstance(facet, str) else None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _is_leaf(cond: Any) -> bool:
|
|
307
|
+
return isinstance(cond, Compare | Window | InSet | Known)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _keep_row(row: Any, qualifiers: EvidenceQualifiers, measured: frozenset[str] | None) -> bool:
|
|
311
|
+
if not getattr(row, "verified", False):
|
|
312
|
+
return False
|
|
313
|
+
if measured is not None and row.measured_by not in measured:
|
|
314
|
+
return False
|
|
315
|
+
if qualifiers.effort is not None and row.effort != qualifiers.effort:
|
|
316
|
+
return False
|
|
317
|
+
if qualifiers.harness is not None and row.harness != qualifiers.harness:
|
|
318
|
+
return False
|
|
319
|
+
after = qualifiers.measured_after
|
|
320
|
+
return after is None or (row.date is not None and row.date > after)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class _Run:
|
|
324
|
+
def __init__(self, resolved: Resolved, index: Any) -> None:
|
|
325
|
+
self.resolved = resolved
|
|
326
|
+
self.index = index
|
|
327
|
+
self.facets = resolved.facets
|
|
328
|
+
self.facet_cache: dict[str, Any] = {}
|
|
329
|
+
self.ids = list(index.candidates())
|
|
330
|
+
if len(set(self.ids)) != len(self.ids):
|
|
331
|
+
raise ValueError("snapshot candidates are not unique")
|
|
332
|
+
self.n = len(self.ids)
|
|
333
|
+
self.universe = (1 << self.n) - 1
|
|
334
|
+
self.pos = {cid: i for i, cid in enumerate(self.ids)}
|
|
335
|
+
self.cache: dict[int, Bits] = {}
|
|
336
|
+
self.evidence_conditions: dict[int, bool] = {}
|
|
337
|
+
self.resolved_threshold: dict[int, Any] = {}
|
|
338
|
+
self.penalties: list[SoftPenalty] = []
|
|
339
|
+
self.path = ""
|
|
340
|
+
self.lineup = 0
|
|
341
|
+
#: The capabilities asked about, which ``@direct`` is relative to.
|
|
342
|
+
self.domains = frozenset(resolved.spec.capabilities or {})
|
|
343
|
+
|
|
344
|
+
def _ids_of(self, bits: int) -> tuple[str, ...]:
|
|
345
|
+
ids = []
|
|
346
|
+
while bits:
|
|
347
|
+
bit = bits & -bits
|
|
348
|
+
ids.append(self.ids[bit.bit_length() - 1])
|
|
349
|
+
bits ^= bit
|
|
350
|
+
return tuple(ids)
|
|
351
|
+
|
|
352
|
+
def _life(self, cid: str) -> str:
|
|
353
|
+
life = self.index.lifecycle(cid)
|
|
354
|
+
if life not in ("active", "deprecated", "retired"):
|
|
355
|
+
raise ValueError(f"lifecycle of {cid} is {life!r}, not active, deprecated or retired")
|
|
356
|
+
return life
|
|
357
|
+
|
|
358
|
+
def _facet(self, facet_id: str) -> Any:
|
|
359
|
+
if facet_id in self.facet_cache:
|
|
360
|
+
return self.facet_cache[facet_id]
|
|
361
|
+
try:
|
|
362
|
+
facet = self.facets(facet_id)
|
|
363
|
+
self.facet_cache[facet_id] = facet
|
|
364
|
+
return facet
|
|
365
|
+
except KeyError:
|
|
366
|
+
raise SpecError([Issue(
|
|
367
|
+
None, facet_id, f"unknown facet {facet_id!r}: not in the facet registry", self.path,
|
|
368
|
+
)]) from None
|
|
369
|
+
|
|
370
|
+
def _is_evidence(self, cond: Any) -> bool:
|
|
371
|
+
key = id(cond)
|
|
372
|
+
if key not in self.evidence_conditions:
|
|
373
|
+
facet_id = _facet_of(cond)
|
|
374
|
+
self.evidence_conditions[key] = (
|
|
375
|
+
getattr(cond, "qualifiers", None) is not None
|
|
376
|
+
or (facet_id is not None and not isinstance(cond, Known) and (
|
|
377
|
+
self._facet(facet_id).subject == "evidence" or facet_id.startswith("evidence.")
|
|
378
|
+
))
|
|
379
|
+
)
|
|
380
|
+
return self.evidence_conditions[key]
|
|
381
|
+
|
|
382
|
+
def _unknown_disposition(self, cond: Any, unk_bits: int) -> tuple[int, int, int]:
|
|
383
|
+
"""Split unknown bits into (treat as pass, may_qualify, unverified fail)."""
|
|
384
|
+
if not unk_bits:
|
|
385
|
+
return 0, 0, 0
|
|
386
|
+
explicit = getattr(cond, "unknown", None)
|
|
387
|
+
if explicit == "pass":
|
|
388
|
+
return unk_bits, 0, 0
|
|
389
|
+
if explicit == "fail" or isinstance(cond, Known):
|
|
390
|
+
return 0, 0, unk_bits
|
|
391
|
+
if explicit == "list":
|
|
392
|
+
return 0, unk_bits, 0
|
|
393
|
+
listed = failed = 0
|
|
394
|
+
for cid in self._ids_of(unk_bits):
|
|
395
|
+
bit = 1 << self.pos[cid]
|
|
396
|
+
facets = self._unknown_facets(cond, bit)
|
|
397
|
+
risks = [self._facet(facet_id).risk for facet_id in facets]
|
|
398
|
+
if not risks or any(risk == "governance" for risk in risks):
|
|
399
|
+
failed |= bit
|
|
400
|
+
else:
|
|
401
|
+
listed |= bit
|
|
402
|
+
return 0, listed, failed
|
|
403
|
+
|
|
404
|
+
def _admitted(self, cid: str, cond: Any) -> list[Any]:
|
|
405
|
+
qualifiers = cond.qualifiers or EvidenceQualifiers()
|
|
406
|
+
if qualifiers.direct and not self.index.direct_for(cond.facet, self.domains):
|
|
407
|
+
return []
|
|
408
|
+
measured = _measurers(qualifiers.measured_by)
|
|
409
|
+
rows = self.index.evidence(
|
|
410
|
+
cid, cond.facet,
|
|
411
|
+
measured_by=None if measured is None else set(measured),
|
|
412
|
+
effort=qualifiers.effort,
|
|
413
|
+
harness=qualifiers.harness,
|
|
414
|
+
after=qualifiers.measured_after,
|
|
415
|
+
)
|
|
416
|
+
return [row.value for row in rows if _keep_row(row, qualifiers, measured)]
|
|
417
|
+
|
|
418
|
+
def _reference(self, cond: Compare) -> Any:
|
|
419
|
+
assert isinstance(cond.value, ModelRef)
|
|
420
|
+
ref = cond.value.model
|
|
421
|
+
key = id(cond)
|
|
422
|
+
if key in self.resolved_threshold:
|
|
423
|
+
return self.resolved_threshold[key]
|
|
424
|
+
try:
|
|
425
|
+
if self._is_evidence(cond):
|
|
426
|
+
values = self._admitted(ref, cond)
|
|
427
|
+
got: Any = _MISSING if not values else _shown(values, cond.op)
|
|
428
|
+
else:
|
|
429
|
+
fact = self.index.fact(ref, cond.facet)
|
|
430
|
+
got = fact.value if fact.state == "known" and fact.value is not None else _MISSING
|
|
431
|
+
except KeyError:
|
|
432
|
+
raise SpecError([Issue(
|
|
433
|
+
render_condition(cond), ref, f"model {ref} is not in the snapshot", self.path,
|
|
434
|
+
)]) from None
|
|
435
|
+
self.resolved_threshold[key] = got
|
|
436
|
+
return got
|
|
437
|
+
|
|
438
|
+
def _evidence_bits(self, cond: Any, op: str, arg: Any) -> Bits:
|
|
439
|
+
qualifiers = cond.qualifiers or EvidenceQualifiers()
|
|
440
|
+
measured = _measurers(qualifiers.measured_by)
|
|
441
|
+
indexed = getattr(self.index, "evidence_where", None)
|
|
442
|
+
if indexed is not None:
|
|
443
|
+
return _as_bits(indexed(
|
|
444
|
+
cond.facet,
|
|
445
|
+
op,
|
|
446
|
+
arg,
|
|
447
|
+
measured_by=None if measured is None else set(measured),
|
|
448
|
+
effort=qualifiers.effort,
|
|
449
|
+
harness=qualifiers.harness,
|
|
450
|
+
after=qualifiers.measured_after,
|
|
451
|
+
direct=qualifiers.direct,
|
|
452
|
+
domains=self.domains,
|
|
453
|
+
), self.universe)
|
|
454
|
+
passing = failing = 0
|
|
455
|
+
for i, cid in enumerate(self.ids):
|
|
456
|
+
try:
|
|
457
|
+
values = self._admitted(cid, cond)
|
|
458
|
+
except KeyError:
|
|
459
|
+
raise SpecError([Issue(
|
|
460
|
+
render_condition(cond), cid, f"{cid} is not in the snapshot", self.path,
|
|
461
|
+
)]) from None
|
|
462
|
+
if op == "between":
|
|
463
|
+
low, high = arg
|
|
464
|
+
leg = _judge(values, lambda value: _op(">=", value, low)
|
|
465
|
+
and _op("<=", value, high))
|
|
466
|
+
else:
|
|
467
|
+
leg = _judge(values, lambda value: _op(op, value, arg))
|
|
468
|
+
bit = 1 << i
|
|
469
|
+
if leg == "pass":
|
|
470
|
+
passing |= bit
|
|
471
|
+
elif leg == "fail":
|
|
472
|
+
failing |= bit
|
|
473
|
+
return Bits(passing, failing, self.universe & ~passing & ~failing)
|
|
474
|
+
|
|
475
|
+
def _compare(self, cond: Compare) -> Bits:
|
|
476
|
+
if isinstance(cond.value, ModelRef):
|
|
477
|
+
threshold = self._reference(cond)
|
|
478
|
+
if threshold is _MISSING:
|
|
479
|
+
return Bits(0, 0, self.universe)
|
|
480
|
+
else:
|
|
481
|
+
threshold = cond.value
|
|
482
|
+
if self._is_evidence(cond):
|
|
483
|
+
return self._evidence_bits(cond, cond.op, threshold)
|
|
484
|
+
return _as_bits(self.index.ids_where(cond.facet, cond.op, threshold), self.universe)
|
|
485
|
+
|
|
486
|
+
def _window(self, cond: Window) -> Bits:
|
|
487
|
+
low, high = cond.between
|
|
488
|
+
if self._is_evidence(cond):
|
|
489
|
+
return self._evidence_bits(cond, "between", (low, high))
|
|
490
|
+
lo = _as_bits(self.index.ids_where(cond.facet, ">=", low), self.universe)
|
|
491
|
+
hi = _as_bits(self.index.ids_where(cond.facet, "<=", high), self.universe)
|
|
492
|
+
return bit_and(lo, hi, self.universe)
|
|
493
|
+
|
|
494
|
+
def _set(self, cond: InSet) -> Bits:
|
|
495
|
+
values = cond.in_ if cond.in_ is not None else cond.not_in
|
|
496
|
+
if not values:
|
|
497
|
+
raise ValueError("a set condition has no values")
|
|
498
|
+
if self._is_evidence(cond):
|
|
499
|
+
acc: Bits | None = None
|
|
500
|
+
for value in values:
|
|
501
|
+
bit = self._evidence_bits(cond, "=", value)
|
|
502
|
+
acc = bit if acc is None else bit_or(acc, bit, self.universe)
|
|
503
|
+
assert acc is not None
|
|
504
|
+
bits = acc
|
|
505
|
+
elif getattr(getattr(self._facet(cond.facet), "value_type", None), "kind", None) == "set":
|
|
506
|
+
# A set-valued facet is in {a, b} when it holds a or b, and not in
|
|
507
|
+
# {a, b} when it holds neither. Equality never matches a set.
|
|
508
|
+
bits = _as_bits(
|
|
509
|
+
self.index.ids_where(cond.facet, "contains_any", list(values)), self.universe
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
acc: Bits | None = None
|
|
513
|
+
for value in values:
|
|
514
|
+
bit = _as_bits(self.index.ids_where(cond.facet, "=", value), self.universe)
|
|
515
|
+
acc = bit if acc is None else bit_or(acc, bit, self.universe)
|
|
516
|
+
assert acc is not None
|
|
517
|
+
bits = acc
|
|
518
|
+
if cond.not_in is not None:
|
|
519
|
+
bits = bit_not(bits, self.universe)
|
|
520
|
+
return bits
|
|
521
|
+
|
|
522
|
+
def _known(self, cond: Known) -> Bits:
|
|
523
|
+
bits = _as_bits(self.index.ids_where(cond.known, "known", None), self.universe)
|
|
524
|
+
return Bits(bits.passing, self.universe & ~bits.passing, 0)
|
|
525
|
+
|
|
526
|
+
def _presented(self, cond: Any, raw: Bits) -> Bits:
|
|
527
|
+
explicit = getattr(cond, "unknown", None)
|
|
528
|
+
if explicit is None:
|
|
529
|
+
return raw
|
|
530
|
+
return _collapse(raw, explicit, self.universe)
|
|
531
|
+
|
|
532
|
+
def _combine(self, children: Sequence[Any], op: Callable[[Bits, Bits, int], Bits]) -> Bits:
|
|
533
|
+
parts: list[Bits] = []
|
|
534
|
+
for child in children:
|
|
535
|
+
if child.soft is not None:
|
|
536
|
+
self._eval(child)
|
|
537
|
+
continue
|
|
538
|
+
parts.append(self._presented(child, self._eval(child)))
|
|
539
|
+
if not parts:
|
|
540
|
+
return Bits(self.universe, 0, 0)
|
|
541
|
+
acc = parts[0]
|
|
542
|
+
for part in parts[1:]:
|
|
543
|
+
acc = op(acc, part, self.universe)
|
|
544
|
+
return acc
|
|
545
|
+
|
|
546
|
+
def _eval(self, cond: Any) -> Bits:
|
|
547
|
+
key = id(cond)
|
|
548
|
+
cached = self.cache.get(key)
|
|
549
|
+
if cached is not None:
|
|
550
|
+
return cached
|
|
551
|
+
if isinstance(cond, Compare):
|
|
552
|
+
bits = self._compare(cond)
|
|
553
|
+
elif isinstance(cond, Window):
|
|
554
|
+
bits = self._window(cond)
|
|
555
|
+
elif isinstance(cond, InSet):
|
|
556
|
+
bits = self._set(cond)
|
|
557
|
+
elif isinstance(cond, Known):
|
|
558
|
+
bits = self._known(cond)
|
|
559
|
+
elif isinstance(cond, AnyOf):
|
|
560
|
+
bits = self._combine(cond.any, bit_or)
|
|
561
|
+
elif isinstance(cond, AllOf):
|
|
562
|
+
bits = self._combine(cond.all, bit_and)
|
|
563
|
+
elif isinstance(cond, NotOf):
|
|
564
|
+
child = cond.not_
|
|
565
|
+
if child.soft is not None:
|
|
566
|
+
self._eval(child)
|
|
567
|
+
bits = Bits(self.universe, 0, 0)
|
|
568
|
+
else:
|
|
569
|
+
bits = bit_not(self._presented(child, self._eval(child)), self.universe)
|
|
570
|
+
else:
|
|
571
|
+
raise TypeError(f"not a condition: {type(cond).__name__}")
|
|
572
|
+
self.cache[key] = bits
|
|
573
|
+
if getattr(cond, "soft", None) is not None:
|
|
574
|
+
self._record_penalty(cond, bits)
|
|
575
|
+
return bits
|
|
576
|
+
|
|
577
|
+
def _record_penalty(self, cond: Any, raw: Bits) -> None:
|
|
578
|
+
lineup = self.lineup
|
|
579
|
+
self.penalties.append(SoftPenalty(
|
|
580
|
+
condition=render_condition(cond),
|
|
581
|
+
penalty=cond.soft.penalty,
|
|
582
|
+
failing=self._ids_of(raw.failing & lineup),
|
|
583
|
+
unknown=self._ids_of(raw.unknown & lineup),
|
|
584
|
+
))
|
|
585
|
+
|
|
586
|
+
def _decisive(self, cond: Any, bit: int, leg: Leg) -> Any:
|
|
587
|
+
if isinstance(cond, NotOf):
|
|
588
|
+
child = cond.not_
|
|
589
|
+
if child.soft is not None:
|
|
590
|
+
return cond
|
|
591
|
+
flipped: Leg = "unknown" if leg == "unknown" else ("fail" if leg == "pass" else "pass")
|
|
592
|
+
return self._decisive(child, bit, flipped)
|
|
593
|
+
children = _children(cond)
|
|
594
|
+
if not children:
|
|
595
|
+
return cond
|
|
596
|
+
for child in children:
|
|
597
|
+
if child.soft is not None:
|
|
598
|
+
continue
|
|
599
|
+
shown = self._presented(child, self._eval(child))
|
|
600
|
+
if _leg_of(shown, bit) == leg:
|
|
601
|
+
return self._decisive(child, bit, leg)
|
|
602
|
+
for child in children:
|
|
603
|
+
if child.soft is not None:
|
|
604
|
+
continue
|
|
605
|
+
if self._eval(child).unknown & bit:
|
|
606
|
+
return self._decisive(child, bit, "unknown")
|
|
607
|
+
return cond
|
|
608
|
+
|
|
609
|
+
def _unknown_facets(self, cond: Any, bit: int) -> list[str]:
|
|
610
|
+
if isinstance(cond, NotOf):
|
|
611
|
+
return self._unknown_facets(cond.not_, bit)
|
|
612
|
+
children = _children(cond)
|
|
613
|
+
if children:
|
|
614
|
+
found: list[str] = []
|
|
615
|
+
for child in children:
|
|
616
|
+
if child.soft is not None:
|
|
617
|
+
continue
|
|
618
|
+
if self._eval(child).unknown & bit:
|
|
619
|
+
for facet_id in self._unknown_facets(child, bit):
|
|
620
|
+
if facet_id not in found:
|
|
621
|
+
found.append(facet_id)
|
|
622
|
+
return found
|
|
623
|
+
facet_id = _facet_of(cond)
|
|
624
|
+
return [facet_id] if facet_id else []
|
|
625
|
+
|
|
626
|
+
def _value(self, cond: Any, cid: str) -> Any:
|
|
627
|
+
if isinstance(cond, Known):
|
|
628
|
+
try:
|
|
629
|
+
return self.index.fact(cid, cond.known).state
|
|
630
|
+
except KeyError:
|
|
631
|
+
return None
|
|
632
|
+
facet_id = _facet_of(cond)
|
|
633
|
+
if facet_id is None:
|
|
634
|
+
return None
|
|
635
|
+
if self._is_evidence(cond):
|
|
636
|
+
values = self._admitted(cid, cond)
|
|
637
|
+
op = cond.op if isinstance(cond, Compare) else None
|
|
638
|
+
return _shown(values, op) if values else None
|
|
639
|
+
try:
|
|
640
|
+
fact = self.index.fact(cid, facet_id)
|
|
641
|
+
except KeyError:
|
|
642
|
+
return None
|
|
643
|
+
if fact.state != "known" or fact.value is None:
|
|
644
|
+
return None
|
|
645
|
+
return fact.value
|
|
646
|
+
|
|
647
|
+
def _threshold(self, cond: Any) -> Any:
|
|
648
|
+
if isinstance(cond, Compare):
|
|
649
|
+
if isinstance(cond.value, ModelRef):
|
|
650
|
+
got = self.resolved_threshold.get(id(cond), _MISSING)
|
|
651
|
+
if got is _MISSING:
|
|
652
|
+
got = self._reference(cond)
|
|
653
|
+
return None if got is _MISSING else got
|
|
654
|
+
return cond.value
|
|
655
|
+
if isinstance(cond, Window):
|
|
656
|
+
return cond.between
|
|
657
|
+
if isinstance(cond, InSet):
|
|
658
|
+
return tuple(cond.in_ if cond.in_ is not None else cond.not_in or ())
|
|
659
|
+
if isinstance(cond, Known):
|
|
660
|
+
return "known"
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
def _leaf_was_unknown(self, cond: Any, bit: int) -> bool:
|
|
664
|
+
leaf = self._decisive(cond, bit, "fail")
|
|
665
|
+
if not _is_leaf(leaf):
|
|
666
|
+
leaf = self._decisive(cond, bit, "unknown")
|
|
667
|
+
if not _is_leaf(leaf):
|
|
668
|
+
return False
|
|
669
|
+
return bool(self._eval(leaf).unknown & bit)
|
|
670
|
+
|
|
671
|
+
def _cover(self, feasible: int, maybe: int, eliminated: int) -> None:
|
|
672
|
+
if (feasible & maybe) or (feasible & eliminated) or (maybe & eliminated):
|
|
673
|
+
raise RuntimeError("filter partition overlaps")
|
|
674
|
+
if (feasible | maybe | eliminated) != self.universe:
|
|
675
|
+
raise RuntimeError("filter partition does not cover the lineup")
|
|
676
|
+
|
|
677
|
+
def _grain_counts(self, bits: int) -> tuple[int, int]:
|
|
678
|
+
ids = self._ids_of(bits)
|
|
679
|
+
return (
|
|
680
|
+
len({self.index.model_of(cid) for cid in ids}),
|
|
681
|
+
sum(self.index.kind(cid) == "offering" for cid in ids),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def run(self) -> FilterResult:
|
|
685
|
+
wanted = self.resolved.spec.snapshot
|
|
686
|
+
if wanted != "latest" and wanted != self.index.snapshot_id:
|
|
687
|
+
raise SpecError([Issue(
|
|
688
|
+
None, "snapshot",
|
|
689
|
+
f"spec asks for {wanted} but the snapshot is {self.index.snapshot_id}",
|
|
690
|
+
"snapshot",
|
|
691
|
+
)])
|
|
692
|
+
sold = {self.index.model_of(cid) for cid in self.ids
|
|
693
|
+
if self.index.kind(cid) == "offering"}
|
|
694
|
+
represented = retired = 0
|
|
695
|
+
for i, cid in enumerate(self.ids):
|
|
696
|
+
if cid in sold:
|
|
697
|
+
represented |= 1 << i
|
|
698
|
+
elif self._life(cid) == "retired":
|
|
699
|
+
retired |= 1 << i
|
|
700
|
+
eliminations: list[Elimination] = []
|
|
701
|
+
if self.resolved.include_retired:
|
|
702
|
+
self.lineup = self.universe & ~represented
|
|
703
|
+
else:
|
|
704
|
+
self.lineup = self.universe & ~retired & ~represented
|
|
705
|
+
for cid in self._ids_of(retired):
|
|
706
|
+
eliminations.append(Elimination(
|
|
707
|
+
candidate=cid, _condition=_RETIRED_CONDITION, value="retired",
|
|
708
|
+
threshold=("active", "deprecated"), facet="model.lifecycle",
|
|
709
|
+
))
|
|
710
|
+
feasible = self.lineup
|
|
711
|
+
maybe = 0
|
|
712
|
+
eliminated = self.universe & ~self.lineup
|
|
713
|
+
unknown_facets: dict[str, list[str]] = {}
|
|
714
|
+
funnel: list[FunnelCount] = []
|
|
715
|
+
rules = 0 if self.resolved.profile is None else len(self.resolved.profile.rules)
|
|
716
|
+
self._cover(feasible, maybe, eliminated)
|
|
717
|
+
|
|
718
|
+
for index, cond in enumerate(self.resolved.conditions):
|
|
719
|
+
self.path = (f"profile.rules[{index}]" if index < rules
|
|
720
|
+
else f"where[{index - rules}]")
|
|
721
|
+
before = feasible.bit_count()
|
|
722
|
+
models_before, offerings_before = self._grain_counts(feasible)
|
|
723
|
+
if cond.soft is not None:
|
|
724
|
+
self._eval(cond)
|
|
725
|
+
funnel.append(FunnelCount(
|
|
726
|
+
cond, before, before, 0,
|
|
727
|
+
models_before, models_before, offerings_before, offerings_before,
|
|
728
|
+
0, 0,
|
|
729
|
+
))
|
|
730
|
+
continue
|
|
731
|
+
raw = self._eval(cond)
|
|
732
|
+
fe_pass, fe_fail, fe_unk = _split(raw, feasible)
|
|
733
|
+
_mb_pass, mb_fail, mb_unk = _split(raw, maybe)
|
|
734
|
+
fe_as_pass, fe_as_list, fe_as_fail = self._unknown_disposition(cond, fe_unk)
|
|
735
|
+
_mb_as_pass, mb_as_list, mb_as_fail = self._unknown_disposition(cond, mb_unk)
|
|
736
|
+
feasible = fe_pass | fe_as_pass
|
|
737
|
+
new_maybe = fe_as_list
|
|
738
|
+
drop = mb_fail | mb_as_fail
|
|
739
|
+
maybe = (maybe & ~drop) | new_maybe
|
|
740
|
+
new_elim = fe_fail | fe_as_fail | drop
|
|
741
|
+
unverified_bits = fe_as_fail | mb_as_fail
|
|
742
|
+
for cid in self._ids_of(new_maybe | mb_as_list):
|
|
743
|
+
found = unknown_facets.setdefault(cid, [])
|
|
744
|
+
for facet_id in self._unknown_facets(cond, 1 << self.pos[cid]):
|
|
745
|
+
if facet_id not in found:
|
|
746
|
+
found.append(facet_id)
|
|
747
|
+
for cid in self._ids_of(drop):
|
|
748
|
+
unknown_facets.pop(cid, None)
|
|
749
|
+
for cid in self._ids_of(new_elim):
|
|
750
|
+
bit = 1 << self.pos[cid]
|
|
751
|
+
unverified = bool(unverified_bits & bit) or self._leaf_was_unknown(cond, bit)
|
|
752
|
+
leg: Leg = "unknown" if unverified else "fail"
|
|
753
|
+
leaf = self._decisive(cond, bit, leg)
|
|
754
|
+
eliminations.append(Elimination(
|
|
755
|
+
candidate=cid, _condition=cond,
|
|
756
|
+
value=None if unverified else self._value(leaf, cid),
|
|
757
|
+
threshold=self._threshold(leaf),
|
|
758
|
+
facet=_facet_of(leaf),
|
|
759
|
+
unverified=unverified,
|
|
760
|
+
))
|
|
761
|
+
eliminated |= new_elim
|
|
762
|
+
models_after, offerings_after = self._grain_counts(feasible)
|
|
763
|
+
models_maybe, offerings_maybe = self._grain_counts(new_maybe)
|
|
764
|
+
funnel.append(FunnelCount(
|
|
765
|
+
cond, before, feasible.bit_count(), new_maybe.bit_count(),
|
|
766
|
+
models_before, models_after, offerings_before, offerings_after,
|
|
767
|
+
models_maybe, offerings_maybe,
|
|
768
|
+
))
|
|
769
|
+
self._cover(feasible, maybe, eliminated)
|
|
770
|
+
|
|
771
|
+
may_qualify = tuple(
|
|
772
|
+
MayQualify(cid, tuple(unknown_facets.get(cid, ())))
|
|
773
|
+
for cid in self._ids_of(maybe)
|
|
774
|
+
)
|
|
775
|
+
deprecated = tuple(cid for cid in self.ids if self._life(cid) == "deprecated")
|
|
776
|
+
return FilterResult(
|
|
777
|
+
snapshot_id=self.index.snapshot_id,
|
|
778
|
+
feasible=self._ids_of(feasible),
|
|
779
|
+
may_qualify=may_qualify,
|
|
780
|
+
funnel=tuple(funnel),
|
|
781
|
+
eliminated=tuple(eliminations),
|
|
782
|
+
penalties=tuple(self.penalties),
|
|
783
|
+
deprecated=deprecated,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _split(raw: Bits, mask: int) -> tuple[int, int, int]:
|
|
788
|
+
passing = raw.passing & mask
|
|
789
|
+
failing = raw.failing & mask
|
|
790
|
+
unknown = mask & ~passing & ~failing
|
|
791
|
+
return passing, failing, unknown
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def apply(resolved: Resolved, index: Any) -> FilterResult:
|
|
795
|
+
"""Filter ``index`` by the resolved conditions. The same inputs give the same result."""
|
|
796
|
+
return _Run(resolved, index).run()
|