DavidPanSX commited on
Commit
9780f57
·
verified ·
1 Parent(s): 17c8aaa

deploy API

Browse files
Files changed (1) hide show
  1. serving/app/evidence.py +132 -132
serving/app/evidence.py CHANGED
@@ -1,132 +1,132 @@
1
- """
2
- What /v1/model-info says about the served model (blueprint B25): how similarity is computed, and the measured error
3
- rates with their sources. Every rate is copied from an evaluation output in this repository: `source` is the file and
4
- `keys` the path of each rate inside it ("/"-separated; `scale` 100 when the file has percent).
5
- tests/test_serving.py checks the copies against those files when they are present.
6
- Rates are fractions (0.012 = 1.2%). All are for run 4 (outputs/runs/stage2_run4) at its served threshold and margin.
7
- """
8
-
9
- SIMILARITY = {
10
- "measure": "cosine similarity of L2-normalised 256-number embeddings (a dot product); 1.0 = same direction",
11
- "personal_gallery": "/v1/match: each medication is the normalised mean of its enrolment embeddings; a pill takes "
12
- "the most similar medication, `unknown` below the threshold, `ambiguous` when the runner-up "
13
- "is within the margin; the imprint read on the pill can then confirm or overturn it",
14
- "catalogue": "/v1/identify: per product, the best similarity over its catalogue photos (averaged over the photos "
15
- "sent of one pill); the closest products are re-ranked by the imprint read; exact search over every "
16
- "photo, no approximate index",
17
- "not_a_probability": "Similarities are not probabilities: pills that aren't in the catalogue still score about "
18
- "0.93. /v1/identify's `confidence` is the calibrated probability, `verdict` its reading.",
19
- }
20
-
21
- MODEL_VERSION = "stage2_run4" # the rates below hold for this model only
22
- RUN4 = "outputs/runs/stage2_run4/eval/"
23
- GALLERY = {"correct_match": "correct_match", "wrong_match": "wrong_match", "own_flip": "own_flip",
24
- "own_rejected": "own_rejected", "unknown_matched": "unknown_matched", "unknown_rejected": "unknown_rejected"}
25
-
26
- ERROR_RATES = [
27
- {
28
- "id": "personal_gallery",
29
- "what": "/v1/match, a patient with 10 medications and 3 enrolment photos each, on C3PI photos of 200 "
30
- "medicines the model never saw in training",
31
- "rates": {"correct_match": 0.7599, "wrong_match": 0.0004, "own_flip": 0.095, "own_rejected": 0.1447,
32
- "unknown_matched": 0.0377, "unknown_rejected": 0.9483},
33
- "meaning": "wrong_match: named as another of the patient's medicines; own_flip: asked to photograph again; "
34
- "own_rejected: their own pill called unknown; unknown_matched: a pill that isn't theirs named "
35
- "as one of theirs",
36
- "source": RUN4 + "c3pi_holdout.json",
37
- "keys": {k: "open_set/gallery_10/at_calibration/" + v for k, v in GALLERY.items()},
38
- },
39
- {
40
- "id": "personal_gallery_with_imprint",
41
- "what": "As personal_gallery, with the imprint check on (the GPU reader: Florence-2-large, framed as served; "
42
- "the pill's own imprint is read on 47% of photos)",
43
- "rates": {"correct_match": 0.7826, "wrong_match": 0.0006, "own_flip": 0.0721, "unknown_matched": 0.0351},
44
- "source": RUN4 + "imprint_gallery_large_c92_distinct.json",
45
- "keys": {k: "test/with_check/" + k for k in ("correct_match", "wrong_match", "own_flip", "unknown_matched")},
46
- },
47
- {
48
- "id": "personal_gallery_other_phones",
49
- "what": "As personal_gallery on MEDISEG: 32 Hong Kong products, other phones, lighting and backgrounds",
50
- "rates": {"correct_match": 0.1872, "wrong_match": 0.0069, "own_flip": 0.1021, "own_rejected": 0.7038,
51
- "unknown_matched": 0.0306, "unknown_rejected": 0.9309},
52
- "meaning": "Outside C3PI's photo style most of a patient's own pills are called unknown (safe but unhelpful): "
53
- "enrol with the patient's own phone and background",
54
- "source": RUN4 + "mediseg.json",
55
- "keys": {k: "open_set/gallery_10/at_calibration/" + v for k, v in GALLERY.items()},
56
- },
57
- {
58
- "id": "same_ingredient_siblings",
59
- "what": "Both siblings in one gallery (same ingredients, other strength or release form), with the imprint "
60
- "check (GPU reader); look_alike: both have the same recorded shape and colour",
61
- "rates": {"named_as_sibling": 0.0011, "named_as_sibling_look_alike": 0.0058},
62
- "meaning": "A stress test: in look-alike pairs the imprint check settles more ties (correct 53% -> 65%) and "
63
- "occasionally picks the sibling (0.35% -> 0.58%); strength is never confirmed from a photo",
64
- "source": RUN4 + "lookalike_strengths_large_c92_distinct.json",
65
- "keys": {"named_as_sibling": "all/with imprint/named as the sibling",
66
- "named_as_sibling_look_alike": "look_alike/with imprint/named as the sibling"},
67
- "scale": 100,
68
- },
69
- {
70
- "id": "same_maker_imprints",
71
- "what": "Two different medicines from one maker whose imprints share a token (Teva's '93', 'TEVA', 'TV') in one "
72
- "gallery, with the imprint check (GPU reader): 3,149 pairs; look_alike: same recorded shape and colour",
73
- "rates": {"named_as_the_other": 0.0001, "named_as_the_other_look_alike": 0.001,
74
- "named_as_the_other_shared_tv": 0.0015},
75
- "meaning": "Rare overall; '93' pairs 0.00%. The worst pair: tramadol 50 mg (TV 58) named as carvedilol 12.5 mg "
76
- "(TV 7295) on 5.8% of its photos. A token two of the patient's medicines share never confirms either",
77
- "source": RUN4 + "same_maker_imprints_large_c92_distinct.json",
78
- "keys": {"named_as_the_other": "all/with imprint/named as the other",
79
- "named_as_the_other_look_alike": "look_alike/with imprint/named as the other",
80
- "named_as_the_other_shared_tv": "shared 'TV'/with imprint/named as the other"},
81
- "scale": 100,
82
- },
83
- {
84
- "id": "catalogue_identify",
85
- "what": "/v1/identify on held-out C3PI photos of catalogue products (photos not in the catalogue), and on "
86
- "MEDISEG pills that aren't in the catalogue; imprints read by the GPU reader",
87
- "rates": {"top1_right": 0.802, "possible_match_share": 0.758, "possible_match_right": 0.867,
88
- "not_in_catalogue_called_possible_match": 0.07},
89
- "source": RUN4 + "identify_confidence_large_c92.json",
90
- "keys": {"top1_right": "test/top1_right",
91
- "possible_match_share": "test/catalogue_pills_by_verdict/possible_match/share",
92
- "possible_match_right": "test/catalogue_pills_by_verdict/possible_match/top1_right",
93
- "not_in_catalogue_called_possible_match": "test/other_pills_by_verdict/possible_match"},
94
- },
95
- {
96
- "id": "pill_count",
97
- "what": "Stand-in pill finder (/v1/detect, /v1/scan without boxes) on 1,000 one-pill C3PI photos (test case "
98
- "D01), and 2- and 3-pill photos made from them (D02)",
99
- "rates": {"exact_count_1": 0.878, "no_boxes_1": 0.068, "over_count_1": 0.054,
100
- "over_count_while_reliable_1": 0.014, "exact_count_2": 0.86, "exact_count_3": 0.8511},
101
- "meaning": "over_count_while_reliable: an extra box while count_reliable is true. The backend confirms with "
102
- "the patient before recording an extra pill (docs/dose_rules.md)",
103
- "source": "outputs/eval/pill_finder.json",
104
- "keys": {"exact_count_1": "by_pills/1/exact_count", "no_boxes_1": "by_pills/1/no_boxes",
105
- "over_count_1": "by_pills/1/over_count",
106
- "over_count_while_reliable_1": "by_pills/1/over_count_while_reliable",
107
- "exact_count_2": "by_pills/2/exact_count", "exact_count_3": "by_pills/3/exact_count"},
108
- },
109
- {
110
- "id": "input_check",
111
- "what": "Input guard (B23) on a small labelled set of web photos: 14 loose-pill photos, 8 blisters, 7 bottles "
112
- "or boxes, 5 printed labels",
113
- "rates": {"loose_pills_wrongly_refused": 0.0, "blister_not_caught": 0.125, "package_not_caught": 0.143,
114
- "printed_not_caught": 0.0},
115
- "meaning": "A small set: a sanity check, not a measured error rate",
116
- "source": "src/epills/core/inference/input_type.py (docstring)",
117
- "keys": {},
118
- },
119
- ]
120
-
121
-
122
- def error_rates(model_version):
123
- """The rates as /v1/model-info shows them; none for a model they weren't measured on."""
124
- if model_version != MODEL_VERSION:
125
- return []
126
- return [{k: v for k, v in e.items() if k != "scale"} for e in ERROR_RATES]
127
-
128
-
129
- def lookup(data, path):
130
- for key in path.split("/"):
131
- data = data[key]
132
- return data
 
1
+ """
2
+ What /v1/model-info says about the served model (blueprint B25): how similarity is computed, and the measured error
3
+ rates with their sources. Every rate is copied from an evaluation output in this repository: `source` is the file and
4
+ `keys` the path of each rate inside it ("/"-separated; `scale` 100 when the file has percent).
5
+ tests/test_serving.py checks the copies against those files when they are present.
6
+ Rates are fractions (0.012 = 1.2%). All are for run 4 (outputs/runs/stage2_run4) at its served threshold and margin.
7
+ """
8
+
9
+ SIMILARITY = {
10
+ "measure": "cosine similarity of L2-normalised 256-number embeddings (a dot product); 1.0 = same direction",
11
+ "personal_gallery": "/v1/match: each medication is the normalised mean of its enrolment embeddings; a pill takes "
12
+ "the most similar medication, `unknown` below the threshold, `ambiguous` when the runner-up "
13
+ "is within the margin; the imprint read on the pill can then confirm or overturn it",
14
+ "catalogue": "/v1/identify: per product, the best similarity over its catalogue photos (averaged over the photos "
15
+ "sent of one pill); the closest products are re-ranked by the imprint read; exact search over every "
16
+ "photo, no approximate index",
17
+ "not_a_probability": "Similarities are not probabilities: pills that aren't in the catalogue still score about "
18
+ "0.93. /v1/identify's `confidence` is the calibrated probability, `verdict` its reading.",
19
+ }
20
+
21
+ MODEL_VERSION = "stage2_run4@2026-10-09" # the rates below hold for this served model only (its version string)
22
+ RUN4 = "outputs/runs/stage2_run4/eval/"
23
+ GALLERY = {"correct_match": "correct_match", "wrong_match": "wrong_match", "own_flip": "own_flip",
24
+ "own_rejected": "own_rejected", "unknown_matched": "unknown_matched", "unknown_rejected": "unknown_rejected"}
25
+
26
+ ERROR_RATES = [
27
+ {
28
+ "id": "personal_gallery",
29
+ "what": "/v1/match, a patient with 10 medications and 3 enrolment photos each, on C3PI photos of 200 "
30
+ "medicines the model never saw in training",
31
+ "rates": {"correct_match": 0.7599, "wrong_match": 0.0004, "own_flip": 0.095, "own_rejected": 0.1447,
32
+ "unknown_matched": 0.0377, "unknown_rejected": 0.9483},
33
+ "meaning": "wrong_match: named as another of the patient's medicines; own_flip: asked to photograph again; "
34
+ "own_rejected: their own pill called unknown; unknown_matched: a pill that isn't theirs named "
35
+ "as one of theirs",
36
+ "source": RUN4 + "c3pi_holdout.json",
37
+ "keys": {k: "open_set/gallery_10/at_calibration/" + v for k, v in GALLERY.items()},
38
+ },
39
+ {
40
+ "id": "personal_gallery_with_imprint",
41
+ "what": "As personal_gallery, with the imprint check on (the GPU reader: Florence-2-large, framed as served; "
42
+ "the pill's own imprint is read on 47% of photos)",
43
+ "rates": {"correct_match": 0.7826, "wrong_match": 0.0006, "own_flip": 0.0721, "unknown_matched": 0.0351},
44
+ "source": RUN4 + "imprint_gallery_large_c92_distinct.json",
45
+ "keys": {k: "test/with_check/" + k for k in ("correct_match", "wrong_match", "own_flip", "unknown_matched")},
46
+ },
47
+ {
48
+ "id": "personal_gallery_other_phones",
49
+ "what": "As personal_gallery on MEDISEG: 32 Hong Kong products, other phones, lighting and backgrounds",
50
+ "rates": {"correct_match": 0.1872, "wrong_match": 0.0069, "own_flip": 0.1021, "own_rejected": 0.7038,
51
+ "unknown_matched": 0.0306, "unknown_rejected": 0.9309},
52
+ "meaning": "Outside C3PI's photo style most of a patient's own pills are called unknown (safe but unhelpful): "
53
+ "enrol with the patient's own phone and background",
54
+ "source": RUN4 + "mediseg.json",
55
+ "keys": {k: "open_set/gallery_10/at_calibration/" + v for k, v in GALLERY.items()},
56
+ },
57
+ {
58
+ "id": "same_ingredient_siblings",
59
+ "what": "Both siblings in one gallery (same ingredients, other strength or release form), with the imprint "
60
+ "check (GPU reader); look_alike: both have the same recorded shape and colour",
61
+ "rates": {"named_as_sibling": 0.0011, "named_as_sibling_look_alike": 0.0058},
62
+ "meaning": "A stress test: in look-alike pairs the imprint check settles more ties (correct 53% -> 65%) and "
63
+ "occasionally picks the sibling (0.35% -> 0.58%); strength is never confirmed from a photo",
64
+ "source": RUN4 + "lookalike_strengths_large_c92_distinct.json",
65
+ "keys": {"named_as_sibling": "all/with imprint/named as the sibling",
66
+ "named_as_sibling_look_alike": "look_alike/with imprint/named as the sibling"},
67
+ "scale": 100,
68
+ },
69
+ {
70
+ "id": "same_maker_imprints",
71
+ "what": "Two different medicines from one maker whose imprints share a token (Teva's '93', 'TEVA', 'TV') in one "
72
+ "gallery, with the imprint check (GPU reader): 3,149 pairs; look_alike: same recorded shape and colour",
73
+ "rates": {"named_as_the_other": 0.0001, "named_as_the_other_look_alike": 0.001,
74
+ "named_as_the_other_shared_tv": 0.0015},
75
+ "meaning": "Rare overall; '93' pairs 0.00%. The worst pair: tramadol 50 mg (TV 58) named as carvedilol 12.5 mg "
76
+ "(TV 7295) on 5.8% of its photos. A token two of the patient's medicines share never confirms either",
77
+ "source": RUN4 + "same_maker_imprints_large_c92_distinct.json",
78
+ "keys": {"named_as_the_other": "all/with imprint/named as the other",
79
+ "named_as_the_other_look_alike": "look_alike/with imprint/named as the other",
80
+ "named_as_the_other_shared_tv": "shared 'TV'/with imprint/named as the other"},
81
+ "scale": 100,
82
+ },
83
+ {
84
+ "id": "catalogue_identify",
85
+ "what": "/v1/identify on held-out C3PI photos of catalogue products (photos not in the catalogue), and on "
86
+ "MEDISEG pills that aren't in the catalogue; imprints read by the GPU reader",
87
+ "rates": {"top1_right": 0.802, "possible_match_share": 0.758, "possible_match_right": 0.867,
88
+ "not_in_catalogue_called_possible_match": 0.07},
89
+ "source": RUN4 + "identify_confidence_large_c92.json",
90
+ "keys": {"top1_right": "test/top1_right",
91
+ "possible_match_share": "test/catalogue_pills_by_verdict/possible_match/share",
92
+ "possible_match_right": "test/catalogue_pills_by_verdict/possible_match/top1_right",
93
+ "not_in_catalogue_called_possible_match": "test/other_pills_by_verdict/possible_match"},
94
+ },
95
+ {
96
+ "id": "pill_count",
97
+ "what": "Stand-in pill finder (/v1/detect, /v1/scan without boxes) on 1,000 one-pill C3PI photos (test case "
98
+ "D01), and 2- and 3-pill photos made from them (D02)",
99
+ "rates": {"exact_count_1": 0.878, "no_boxes_1": 0.068, "over_count_1": 0.054,
100
+ "over_count_while_reliable_1": 0.014, "exact_count_2": 0.86, "exact_count_3": 0.8511},
101
+ "meaning": "over_count_while_reliable: an extra box while count_reliable is true. The backend confirms with "
102
+ "the patient before recording an extra pill (docs/dose_rules.md)",
103
+ "source": "outputs/eval/pill_finder.json",
104
+ "keys": {"exact_count_1": "by_pills/1/exact_count", "no_boxes_1": "by_pills/1/no_boxes",
105
+ "over_count_1": "by_pills/1/over_count",
106
+ "over_count_while_reliable_1": "by_pills/1/over_count_while_reliable",
107
+ "exact_count_2": "by_pills/2/exact_count", "exact_count_3": "by_pills/3/exact_count"},
108
+ },
109
+ {
110
+ "id": "input_check",
111
+ "what": "Input guard (B23) on a small labelled set of web photos: 14 loose-pill photos, 8 blisters, 7 bottles "
112
+ "or boxes, 5 printed labels",
113
+ "rates": {"loose_pills_wrongly_refused": 0.0, "blister_not_caught": 0.125, "package_not_caught": 0.143,
114
+ "printed_not_caught": 0.0},
115
+ "meaning": "A small set: a sanity check, not a measured error rate",
116
+ "source": "src/epills/core/inference/input_type.py (docstring)",
117
+ "keys": {},
118
+ },
119
+ ]
120
+
121
+
122
+ def error_rates(model_version):
123
+ """The rates as /v1/model-info shows them; none for a model they weren't measured on."""
124
+ if model_version != MODEL_VERSION:
125
+ return []
126
+ return [{k: v for k, v in e.items() if k != "scale"} for e in ERROR_RATES]
127
+
128
+
129
+ def lookup(data, path):
130
+ for key in path.split("/"):
131
+ data = data[key]
132
+ return data