open-standards/aac/conformance/v1/adaptive.json
Transaction Science edcf02b350 feat(access-core, aac-core): Bayesian selection that never moves a cell, and prediction in 43 languages
Selection (access-core; SPEC §5.4, proposed): a prior over the options on the
board — usage frequency now, the Laya decision model later — times the
person's own switch-timing likelihood, after Nomon (Broderick & MacKay):
- press_timing.rs learns a damped kernel-density estimate of each press's
  offset from the intended highlight, from a population prior scaled to the
  person's scan interval, and only from selections the person keeps: presses
  are staged per attempt, dropped when abandoned or undone (including an Undo
  reached by scanning), and learned only after three newer selections.
- selection.rs computes the posterior (late presses explained as late, a
  2% lapse term), selects at 99:1 odds with the prior capped so presses must
  favour the choice 4:1, and otherwise keeps scanning — a row gets up to two
  extra passes. Without opt-in, or before 10 kept presses, it behaves exactly
  like the conventional scanner and only measures.
- scan_order.rs orders rows, groups and cells by prior mass (fewest expected
  steps) without moving any; options keep stable position-order ids, and a
  new order applies from the next pass. dwell.rs weights per-cell dwell from
  0.8x to 1.5x within hard bounds. Every adaptation is opt-in and previewable.
Conformance C8–C11 (order never moves a position, opt-in required, dwell
bounds, selection follows presses): 370/370 with the original 236.

Prediction (aac-core): per-language packs — word frequencies, word pairs,
sentence starts and a letter model, front-coded and CRC-checked — built
reproducibly by tools/lang/build_packs.py from pinned wordfreq 3.2.0
(CC-BY-SA-4.0), FrequencyWords 2018 (CC-BY-SA-4.0) and Common Voice sentences
(CC0), into data/lang/ (43 of 45 languages; gu and kn have no open source
large enough and report "adaptive only"). PredictionEngine blends a pack with
what the person has said (hierarchical Dirichlet smoothing): next word,
completion, and letter prediction for letter scanning, deterministically.
A per-language pair filter exists; its lists still need curating before the
app shows next-word suggestions to children.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 11:28:26 -04:00

1611 lines
26 KiB
JSON

{
"dwell_cases": [
{
"dwell_ms": 800,
"expected_bounds_ms": [
640,
1200
],
"expected_dwell_ms": [
[
0,
0,
699
],
[
0,
1,
864
],
[
0,
2,
1032
],
[
0,
3,
1200
]
],
"max_dwell_ms": 2000,
"name": "800 ms, a skewed prior",
"weights": [
[
0,
0,
0.7
],
[
0,
1,
0.2
],
[
0,
2,
0.1
],
[
0,
3,
0.0
]
]
},
{
"dwell_ms": 800,
"expected_bounds_ms": [
640,
1200
],
"expected_dwell_ms": [
[
0,
0,
800
],
[
0,
1,
800
],
[
1,
0,
800
],
[
1,
1,
800
]
],
"max_dwell_ms": 2000,
"name": "a uniform prior changes nothing",
"weights": [
[
0,
0,
1.0
],
[
0,
1,
1.0
],
[
1,
0,
1.0
],
[
1,
1,
1.0
]
]
},
{
"dwell_ms": 200,
"expected_bounds_ms": [
200,
300
],
"expected_dwell_ms": [
[
0,
0,
200
],
[
0,
1,
216
],
[
0,
2,
258
],
[
0,
3,
300
]
],
"max_dwell_ms": 2000,
"name": "the person chose 200 ms: never below it",
"weights": [
[
0,
0,
0.7
],
[
0,
1,
0.2
],
[
0,
2,
0.1
],
[
0,
3,
0.0
]
]
},
{
"dwell_ms": 320,
"expected_bounds_ms": [
300,
480
],
"expected_dwell_ms": [
[
0,
0,
300
],
[
0,
1,
346
],
[
0,
2,
413
],
[
0,
3,
480
]
],
"max_dwell_ms": 2000,
"name": "320 ms: never below the proposal floor",
"weights": [
[
0,
0,
0.7
],
[
0,
1,
0.2
],
[
0,
2,
0.1
],
[
0,
3,
0.0
]
]
},
{
"dwell_ms": 1600,
"expected_bounds_ms": [
1280,
2000
],
"expected_dwell_ms": [
[
0,
0,
1398
],
[
0,
1,
1728
],
[
0,
2,
2000
],
[
0,
3,
2000
]
],
"max_dwell_ms": 2000,
"name": "1600 ms: never above the ceiling",
"weights": [
[
0,
0,
0.7
],
[
0,
1,
0.2
],
[
0,
2,
0.1
],
[
0,
3,
0.0
]
]
},
{
"dwell_ms": 100,
"expected_bounds_ms": [
100,
150
],
"expected_dwell_ms": [
[
0,
0,
100
],
[
0,
1,
108
],
[
0,
2,
129
],
[
0,
3,
150
]
],
"max_dwell_ms": 2000,
"name": "a dwell below the 150 ms floor is never weighted below itself",
"weights": [
[
0,
0,
0.7
],
[
0,
1,
0.2
],
[
0,
2,
0.1
],
[
0,
3,
0.0
]
]
}
],
"order_cases": [
{
"cols": 4,
"expected_cell_sequence": [
[
2,
3
],
[
2,
0
],
[
2,
1
],
[
2,
2
],
[
0,
1
],
[
0,
0
],
[
0,
2
],
[
0,
3
],
[
1,
0
],
[
1,
1
],
[
1,
2
],
[
1,
3
]
],
"expected_members": [
[
1,
0,
2,
3
],
[
0,
1,
2,
3
],
[
3,
0,
1,
2
]
],
"expected_units": [
2,
0,
1
],
"mode": "RowColumn",
"name": "row-column: the likeliest row and cell first",
"rows": 3,
"weights": [
[
2,
3,
0.5
],
[
2,
0,
0.2
],
[
0,
1,
0.3
]
]
},
{
"cols": 3,
"expected_cell_sequence": [
[
0,
0
],
[
0,
1
],
[
0,
2
],
[
1,
0
],
[
1,
1
],
[
1,
2
]
],
"expected_members": [
[
0,
1,
2
],
[
0,
1,
2
]
],
"expected_units": [
0,
1
],
"mode": "RowColumn",
"name": "row-column: a uniform prior is the fixed order",
"rows": 2,
"weights": [
[
0,
0,
1.0
],
[
0,
1,
1.0
],
[
0,
2,
1.0
],
[
1,
0,
1.0
],
[
1,
1,
1.0
],
[
1,
2,
1.0
]
]
},
{
"cols": 3,
"expected_cell_sequence": [
[
0,
0
],
[
0,
1
],
[
0,
2
],
[
1,
1
],
[
1,
0
],
[
1,
2
],
[
2,
2
],
[
2,
0
],
[
2,
1
]
],
"expected_members": [
[
0,
1,
2
],
[
1,
0,
2
],
[
2,
0,
1
]
],
"expected_units": [
0,
1,
2
],
"mode": "RowColumn",
"name": "row-column: ties keep position order",
"rows": 3,
"weights": [
[
0,
0,
1.0
],
[
1,
1,
1.0
],
[
2,
2,
1.0
]
]
},
{
"cols": 3,
"expected_cell_sequence": [
[
1,
2
],
[
0,
1
],
[
0,
0
],
[
0,
2
],
[
1,
0
],
[
1,
1
]
],
"expected_members": [],
"expected_units": [
5,
1,
0,
2,
3,
4
],
"mode": "Linear",
"name": "linear: unweighted cells go last, in position order",
"rows": 2,
"weights": [
[
1,
2,
0.6
],
[
0,
1,
0.4
]
]
},
{
"cols": 2,
"expected_cell_sequence": [
[
0,
0
],
[
0,
1
],
[
1,
0
],
[
1,
1
]
],
"expected_members": [],
"expected_units": [
0,
1,
2,
3
],
"mode": "Linear",
"name": "linear: no prior is the fixed order",
"rows": 2,
"weights": []
},
{
"cols": 5,
"expected_cell_sequence": [
[
3,
4
],
[
3,
0
],
[
3,
1
],
[
3,
2
],
[
3,
3
],
[
1,
0
],
[
1,
1
],
[
1,
2
],
[
1,
3
],
[
1,
4
],
[
0,
0
],
[
0,
1
],
[
0,
2
],
[
0,
3
],
[
0,
4
],
[
2,
0
],
[
2,
1
],
[
2,
2
],
[
2,
3
],
[
2,
4
]
],
"expected_members": [
[
0,
1,
2,
3,
4
],
[
0,
1,
2,
3,
4
],
[
0,
1,
2,
3,
4
],
[
4,
0,
1,
2,
3
]
],
"expected_units": [
3,
1,
0,
2
],
"mode": "Group",
"name": "group: the likeliest group first, membership unchanged",
"rows": 4,
"weights": [
[
3,
4,
0.7
],
[
1,
0,
0.3
]
]
},
{
"cols": 5,
"expected_cell_sequence": [
[
0,
4
],
[
1,
4
],
[
3,
3
],
[
2,
2
],
[
2,
3
],
[
3,
2
],
[
0,
0
],
[
0,
1
],
[
1,
0
],
[
1,
1
],
[
0,
2
],
[
0,
3
],
[
1,
2
],
[
1,
3
],
[
2,
0
],
[
2,
1
],
[
3,
0
],
[
3,
1
],
[
2,
4
],
[
3,
4
]
],
"expected_members": [
[
0,
1,
2,
3
],
[
0,
1,
2,
3
],
[
0,
1
],
[
0,
1,
2,
3
],
[
3,
0,
1,
2
],
[
0,
1
]
],
"expected_units": [
2,
4,
0,
1,
3,
5
],
"mode": "Block",
"name": "block: a half-block at the odd edge holds the likeliest cell",
"rows": 4,
"weights": [
[
0,
4,
0.9
],
[
3,
3,
0.1
]
]
}
],
"rule": {
"adaptive_order_default": false,
"bayesian_selection_default": false,
"dwell": {
"absolute_floor_ms": 150,
"bounds": "lo = max(ceil(0.8 D), min(D, 300)) (>= 150 for any D >= 150); hi = max(min(floor(1.5 D), max_dwell_ms), D, lo)",
"dwell": "clamp(round(D * factor), lo, hi)",
"factor": "x = ln(n p / sum p): x >= 0 -> 1 - 0.2 tanh(x / ln 4); x < 0 -> 1 + 0.5 tanh(-x / ln 4); p = 0 -> 1.5; all zero -> 1",
"max_factor": 1.5,
"min_factor": 0.8,
"person_dwell": "applying weights never changes D, never proposes, and never resets the section 5.3 measurement window; weights applied during a dwell take effect when it ends; opting out restores D at once and forgets the weights",
"proposal_floor_ms": 300
},
"dwell_weighting_default": false,
"order": "units (rows | cells | groups | blocks) by descending sum of weights, members by descending weight; ties by position (row-major); unit membership is the fixed scan's",
"selection": {
"damping": 0.96,
"decision": "select the leader when ln-odds over the runner-up >= ln win_odds; a lone option when sum ln L - n ln(lapse / S) >= ln(win_odds / prior_odds_cap); else keep scanning; ties to the lower index; not opted in or fewer than min_confirmed kept presses: the latest highlight at or before the last press",
"density": "f(x) = [w0 N(x; 0.65 T, max(0.25 * 0.65 T, 10)) + sum_i d^i N(x; y_i, h)] / (w0 + sum_i d^i); y_0 newest; w0 = 25 d^n; h = max(10, 1.06 sd n_w^-0.2) with sd, n_w the weighted spread and total weight of prior and samples",
"history": 95,
"keep_scanning": "a level whose pass ends after an undecided press gets another pass, at most 2 per visit",
"kernel_evaluations": "presses x onsets in the window x (kept samples + 1)",
"lapse": 0.02,
"learning": "presses are staged per selection across its levels; a top-level choice discards what was staged before it; on the final cell the staged presses become a pending selection; an Undo reached by scanning discards itself and the selection it undid; a selection is learned once undo_depth + 1 newer selections are completed; of a Bayesian selection only presses with (1 - lapse) f(offset) > lapse / S are learned",
"likelihood": "L_j(t) = (1 - lapse) max_c f(t - onset_{j,c}) + lapse / S over onsets with -T/2 <= t - onset < 2T (none: f = 0); S = last onset - first onset + T",
"min_bandwidth_ms": 10.0,
"min_confirmed": 10,
"offset_window_intervals": [
-0.5,
2.0
],
"options": "an option is its position-order index at its level, never its rank in a pass",
"population_mean_fraction_of_interval": 0.65,
"population_sd_fraction_of_mean": 0.25,
"prior": "normalise; floor each at max / prior_odds_cap; renormalise",
"prior_odds_cap": 24.75,
"prior_weight": 25.0,
"rule_in_force": "a rule with win_odds / prior_odds_cap < 4 (or non-finite, win_odds <= 1, prior_odds_cap < 1) is replaced by the default",
"undo_depth": 2,
"win_odds": 99.0
}
},
"selection_cases": [
{
"expected_decision": {
"kind": "select",
"method": "bayesian",
"option": 1
},
"expected_kernel_evaluations": 26,
"expected_posterior": [
0.003177427738009439,
0.9862701647005641,
0.0041975520854075285,
0.003177427738009439,
0.003177427738009439
],
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "a late press selects the row before",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
],
[
3,
3000.0
],
[
4,
4000.0
]
],
"opted_in": true,
"presses_ms": [
2040.0
],
"priors": [
0.2,
0.2,
0.2,
0.2,
0.2
]
},
{
"expected_decision": {
"kind": "select",
"method": "bayesian",
"option": 2
},
"expected_kernel_evaluations": 39,
"expected_posterior": [
0.0011111395652139541,
0.0011111420138189177,
0.9955554368419343,
0.0011111420138189177,
0.0011111395652139541
],
"interval_ms": 1000,
"learned_offsets_ms": [
640.0,
660.0,
630.0,
670.0,
650.0,
655.0,
645.0,
665.0,
635.0,
650.0,
660.0,
640.0
],
"name": "an on-time press agrees with the highlight",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
],
[
3,
3000.0
],
[
4,
4000.0
]
],
"opted_in": true,
"presses_ms": [
2650.0
],
"priors": [
0.2,
0.2,
0.2,
0.2,
0.2
]
},
{
"expected_decision": {
"kind": "keep_scanning",
"leader": 0
},
"expected_kernel_evaluations": 13,
"expected_posterior": [
0.9090563569246255,
0.030314547691791503,
0.030314547691791503,
0.030314547691791503
],
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "a weakly explained press keeps scanning",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
],
[
3,
3000.0
]
],
"opted_in": true,
"presses_ms": [
300.0
],
"priors": [
0.25,
0.25,
0.25,
0.25
]
},
{
"expected_decision": {
"kind": "keep_scanning",
"leader": 0
},
"expected_kernel_evaluations": 0,
"expected_posterior": [
0.925233644859813,
0.037383177570093434,
0.037383177570093434
],
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "the prior alone never selects",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
]
],
"opted_in": true,
"presses_ms": [
60000.0
],
"priors": [
1.0,
0.0,
0.0
]
},
{
"expected_decision": {
"kind": "select",
"method": "bayesian",
"option": 1
},
"expected_kernel_evaluations": 52,
"expected_posterior": [
0.00006442312301447224,
0.9999233520609805,
7.01890707459316e-6,
2.6029544652312e-6,
2.6029544652312e-6
],
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "two presses overrule the strongest allowed prior",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
],
[
3,
3000.0
],
[
4,
4000.0
],
[
0,
5000.0
],
[
1,
6000.0
],
[
2,
7000.0
],
[
3,
8000.0
],
[
4,
9000.0
]
],
"opted_in": true,
"presses_ms": [
2040.0,
7040.0
],
"priors": [
1.0,
0.0,
0.0,
0.0,
0.0
]
},
{
"expected_decision": {
"kind": "select",
"method": "not_opted_in",
"option": 2
},
"expected_kernel_evaluations": 0,
"expected_posterior": null,
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "not opted in: what was highlighted",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
],
[
3,
3000.0
],
[
4,
4000.0
]
],
"opted_in": false,
"presses_ms": [
2040.0
],
"priors": [
0.2,
0.2,
0.2,
0.2,
0.2
]
},
{
"expected_decision": {
"kind": "select",
"method": "model_not_trusted",
"option": 2
},
"expected_kernel_evaluations": 0,
"expected_posterior": null,
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0
],
"name": "nine kept presses: what was highlighted",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
],
[
3,
3000.0
],
[
4,
4000.0
]
],
"opted_in": true,
"presses_ms": [
2040.0
],
"priors": [
0.2,
0.2,
0.2,
0.2,
0.2
]
},
{
"expected_decision": {
"kind": "keep_scanning",
"leader": null
},
"expected_kernel_evaluations": 0,
"expected_posterior": null,
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "a press before any highlight selects nothing",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
],
[
2,
2000.0
],
[
3,
3000.0
],
[
4,
4000.0
]
],
"opted_in": false,
"presses_ms": [
-50.0
],
"priors": [
0.2,
0.2,
0.2,
0.2,
0.2
]
},
{
"expected_decision": {
"kind": "keep_scanning",
"leader": 1
},
"expected_kernel_evaluations": 13,
"expected_posterior": [
0.4958748075950443,
0.5041251924049557
],
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "an onset more than two intervals away explains nothing",
"onsets": [
[
0,
0.0
],
[
1,
1000.0
]
],
"opted_in": true,
"presses_ms": [
2600.0
],
"priors": [
0.5,
0.5
]
},
{
"expected_decision": {
"kind": "select",
"method": "bayesian",
"option": 0
},
"expected_kernel_evaluations": 13,
"expected_posterior": [
1.0
],
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "a lone option selects on a press it explains",
"onsets": [
[
0,
0.0
]
],
"opted_in": true,
"presses_ms": [
1050.0
],
"priors": [
1.0
]
},
{
"expected_decision": {
"kind": "keep_scanning",
"leader": 0
},
"expected_kernel_evaluations": 0,
"expected_posterior": [
1.0
],
"interval_ms": 1000,
"learned_offsets_ms": [
1040.0,
1060.0,
1020.0,
1080.0,
1050.0,
1030.0,
1070.0,
1045.0,
1055.0,
1065.0,
1035.0,
1050.0
],
"name": "a lone option does not select on a press it cannot explain",
"onsets": [
[
0,
0.0
]
],
"opted_in": true,
"presses_ms": [
5000.0
],
"priors": [
1.0
]
}
],
"spec": "SPEC.md §3, §5.1, §5.4 (proposed, after 0.2.0)",
"standard": "aac",
"vector": "adaptive",
"version": "0.3.0-draft"
}