# sym-int8: mirror symmetry and int8 bps offsets ## Method **Chosen: adaptive100, 10,024 bytes total; report-set loss -0.008644684, SE 0.007750660, n=2048.** The 12 KB and mean-loss <0.2 targets are met on the replica. Negative loss means the decoded policy scored higher. Credit: Taylor (gtmcknight), solved policy and replica, commit `4812a6382311624e1506aca131ca35c378f930d9`. Read [README](https://raw.githubusercontent.com/gtmcknight/amm-sim/4812a6382311624e1506aca131ca35c378f930d9/research/solved/README.md) first. Source policy tables are [pol_mid.json](https://raw.githubusercontent.com/gtmcknight/amm-sim/4812a6382311624e1506aca131ca35c378f930d9/research/solved/pol_mid.json); SHA-256 `efbb5ee102db4d312f64565d9bf7232b6becbe02b624164f744a402bbbf2983b`, 780,061 bytes. All experimental numbers below are local measurements using those pinned files, not claims of Rust-engine performance. ### Original policy inventory Values are quote offsets from estimated fair price, in bps. The six policy tables contain 71,262 values. Rows in every table are `w=(i-160)*5` bps, i=0..320, from -800 to +800. A is ask; B is bid. The policy accesses these six arrays through [DPPolicy](https://raw.githubusercontent.com/gtmcknight/amm-sim/4812a6382311624e1506aca131ca35c378f930d9/research/solved/evalpol.py). | Table | Shape (w × R) | Minimum bps | Maximum bps | R axis | |---|---|---:|---:|---| | A0 | 321 × 13 | -24.262 | 800 | -30..30 by 5 bps | | B0 | 321 × 13 | -24.262 | 800 | -30..30 by 5 bps | | A1 | 321 × 49 | -31.323 | 800 | 5 × W2L below | | B1 | 321 × 49 | -31.323 | 800 | 5 × W2L below | | A2 | 321 × 49 | -31.333 | 800 | 5 × W2L below | | B2 | 321 × 49 | -31.333 | 800 | 5 × W2L below | `W2L = [-160,-130,-100,-80,-60,-50,-40,-30,-25,-20,-17,-14,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,14,17,20,25,30,40,50,60,80,100,130,160]`. The nonuniform R grids span -800..800 bps; successive bps steps are 5 times successive differences in this list (5, 10, 15, 25, 50, 100, or 150 bps, depending on location). No R columns are removed. `VD0` and `VD1` are value-function arrays, not policy quote tables. They are omitted from the decoder output; all six fee arrays and every non-array grid/solve metadata field are restored in the original JSON schema accepted by `real.py`. ### Gate P and symmetry, before compression Unmodified `python3 -I gates.py` reproduced [the pinned gates](https://raw.githubusercontent.com/gtmcknight/amm-sim/4812a6382311624e1506aca131ca35c378f930d9/research/solved/gates.py) before creating the encoding: ```text seed perfect-info gaussian+plain gaussian+qm lattice+qm 10000 448.261189 443.455598 443.522608 443.663520 10001 576.779199 571.463409 571.623551 571.977293 10002 531.886497 525.168028 525.865859 526.411631 ``` Exhaustive comparison `abs(Ap[i][j] - Bp[320-i][nc-1-j])` across every cell gave maximum **0.000 bps** in each phase (and globally). Both axes are sign-symmetric, including W2L. Store A only, reconstruct B by reversing both axes. This halves the full policy table set without approximation. For every retained A row, base = Python `round((min(row)+max(row))/2)` bps; signed residual = `clip(round(value-base), -128, 127)` at **1 bps per unit**. `round` uses ties to even. The largest within-row spans before sampling are 47.699, 160, and 150 bps in phases 0, 1, and 2, so **zero residuals clip in every candidate**. The base is signed int16 bps. Fill missing w rows by linear interpolation of decoded retained rows, then mirror to form B. Original bilinear lookup and all Gaussian filter logic stay unchanged. ## Encoding One representation only: one-sided symmetric tables, integer row bases, signed int8 bps residuals; row subsampling is the tuned parameter. No entropy coding, low-rank model, or second codec. All integers are little-endian; residuals use two's complement. No padding. Exact chosen layout: | Byte offsets, inclusive | Content | |---|---| | 0..3 | ASCII `SI81` | | 4..5 | uint16 JSON header length H=377 | | 6..7 | uint16 retained row count K=81 | | 8..384 | H bytes UTF-8 compact JSON containing original scalar metadata and W2L | | 385..546 | K uint16 original w-row indices, sorted ascending | | 547..1761 | phase 0: 81 records of 15 bytes each | | 1762..5892 | phase 1: 81 records of 51 bytes each | | 5893..10023 | phase 2: 81 records of 51 bytes each | Each record is `[int16 base][int8 residual column 0]...[int8 residual column nc-1]`, nc=13 for phase 0, 49 otherwise. Records follow retained row order; column order is the original R grid. For phase start S and retained row index k, base address is `S+k*(nc+2)`; residual (k,j) is at `S+k*(nc+2)+2+j`. Offset value is `sign16(base)+sign8(residual)` bps. Header magic is versioned; K comes from the header, not the magic's digits. Total = 8 prefix + 377 metadata + 162 row indices + 8,991 residual bytes + 486 base bytes = **10,024 bytes**, including everything needed to decode. Raw numeric table payload is 9,477 bytes. Solidity can hardcode the metadata, but this report does not subtract it from the total. Base64 textual expansion is not deployed data. Chosen w knots are the sorted union (inclusive) of `[-100,100] step 5`, `[-200,200] step 10`, `[-400,400] step 40`, `[-800,800] step 100`, in bps. Union is literal: overlapping ranges can add intermediate knots. Convert each w to original index `(w+800)//5`. Exact indices: ```text 0,20,40,60,80,88,96,100,104,112,120,122,124,126,128,130,132,134, 136,138,140,141,142,143,144,145,146,147,148,149,150,151,152,153, 154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169, 170,171,172,173,174,175,176,177,178,179,180,182,184,186,188,190, 192,194,196,198,200,208,216,220,224,232,240,260,280,300,320 ``` For Solidity, bracket w between retained knots and R between original columns, clamp to axis endpoints, decode four corners, then interpolate. For bid, perform the ask lookup with both coordinates negated. Direct interpolation on retained knots equals interpolation in the decoder's expanded grid, up to floating-point roundoff, since retained knots are original grid points. ## Tradeoff All choices use exact symmetry and the same 1 bps int8 quantizer. Choose seeds **60000..61023 only**, n=1024 for every row. Loss per seed is `full.edge - decoded.edge`. SE is sample standard deviation of those paired losses divided by sqrt(n). The full and decoded policies see the same per-seed price, retail and configuration random streams, each over the replica's default 10,000 steps ([replica2.py](https://raw.githubusercontent.com/gtmcknight/amm-sim/4812a6382311624e1506aca131ca35c378f930d9/research/solved/replica2.py)). | Setting | Retained rows | Bytes, entire blob | Choose-set loss | SE | |---|---:|---:|---:|---:| | uniform20 | 81 | 10,024 | +0.322883175 | 0.014155512 | | adaptive100 | 81 | 10,024 | -0.001281364 | 0.010820910 | | adaptive180 | 97 | 11,928 | -0.000867544 | 0.010711970 | `uniform20`: w=-800..800 every 20 bps. `adaptive100`: union above. `adaptive180`: replace the first range by [-180,180] step 5; leave the other three ranges unchanged. Each used a freshly generated blob and decoded JSON. No candidate uses qm tables. Select **adaptive100**: smallest mean choose loss, lower bytes than adaptive180, and a clear improvement over uniform20. The small difference between the two adaptive candidates is below their sampling uncertainty; choose100 has no evidence of a meaningful disadvantage. The initial 16-seed plumbing smoke test used only 60000..60015, a subset of the selection set. It was not an additional seed set. Choose-set worst five paired losses for each setting (seed: full-minus-decoded edge): | Setting | Worst five, descending loss | |---|---| | uniform20 | 60092: 1.652588151; 60816: 1.616190352; 60434: 1.555479497; 60084: 1.525769795; 60122: 1.516516356 | | adaptive100 | 60411: 1.157516225; 60304: 0.978722026; 60092: 0.926825374; 60244: 0.913889322; 60723: 0.894010861 | | adaptive180 | 60411: 1.118144900; 60038: 1.094758424; 60542: 1.073655936; 60244: 1.010233475; 60385: 0.918848403 | ## Chosen Frozen before report scoring: **adaptive100**, 1 bps residual units, signed int16 row bases, 81 retained rows, **10,024 bytes**, no clipping. Report seeds **10000..12047**, **n=2048**; run once as the selected representation, with full-table comparisons on the identical seeds: - Full mean edge: **532.385688969**. - Decoded mean edge: **532.394333653**. - Mean paired loss (full minus decoded): **-0.008644684**. - Paired SE: **0.007750660**. Worst five seeds are the largest positive full-minus-decoded losses, not the five lowest absolute scores: | Seed | Loss | Full edge | Decoded edge | |---:|---:|---:|---:| | 10429 | 1.317202325 | 593.861185681 | 592.543983356 | | 10177 | 1.141135028 | 652.294634992 | 651.153499963 | | 10041 | 1.036806884 | 628.992913052 | 627.956106168 | | 11826 | 1.018917236 | 631.554259203 | 630.535341967 | | 10231 | 0.987694005 | 692.630207081 | 691.642513076 | No parameter changes followed reading the report set. This is a measured mean-loss result, not a bound on individual losses. Over all 71,262 reconstructed quote cells, maximum absolute offset error is 39.868 bps and mean absolute error is 0.164118464 bps. These table errors include coarse far-tail row interpolation. They are not callback fee-equivalence errors. ### Scoring and reproduction The scoring code uses the unmodified [real.RealPolicy](https://raw.githubusercontent.com/gtmcknight/amm-sim/4812a6382311624e1506aca131ca35c378f930d9/research/solved/real.py), `{'soft':1.0}`, and unmodified `replica2.run`. It saves the edge for every seed and pairs by seed, rather than subtracting independently rounded printed means. All Taylor modules were executed with plain `python3 -I`; no dependencies or environment setup were installed. Local helper scripts add the pinned solved directory to sys.path, just as Taylor's own files do. Python 3.14 defaults to forkserver; this sandbox refuses the Unix socket it needs. Used `multiprocessing.set_start_method('fork')` in a wrapper, leaving Taylor's files unchanged. For the official CLI report, from the pinned `research/solved` directory, with decoded JSON saved as `../../../work/adaptive100.json`, the exact commands are: ```sh python3 -I -c "import multiprocessing,runpy,sys; multiprocessing.set_start_method('fork'); sys.argv=['real.py','pol_mid.json','plain:soft=1']; runpy.run_path('real.py',run_name='__main__')" python3 -I -c "import multiprocessing,runpy,sys; multiprocessing.set_start_method('fork'); sys.argv=['real.py','../../../work/adaptive100.json','plain:soft=1']; runpy.run_path('real.py',run_name='__main__')" ``` The official CLI printed full edge 532.386, vs HA +2.828 SE 0.041; decoded edge 532.394, vs HA +2.837 SE 0.041. These rounded HA comparisons are not the paired compression-loss estimator. The CLI hardcodes report seeds starting at 10000 and prints comparisons with HA, not individual rows or arbitrary start seeds. For selection and exact paired losses, the equivalent per-seed computation was: ```python # Launch under python3 -I; insert the absolute pinned research/solved directory first. import sys, json, math sys.path.insert(0, SOLVED) import real, replica2 from evalpol import load def score(path, seed): return replica2.run(seed, real.RealPolicy(load(path), {'soft': 1.0}))['edge'] # Selection: range(60000,61024); report: range(10000,12048). # full and decoded are paths to pol_mid.json and decoder output. loss = [score(full, s)-score(decoded, s) for s in seeds] n = len(loss) mean = sum(loss)/n se = math.sqrt(sum((x-mean)**2 for x in loss)/(n*(n-1))) ``` Actual execution used a 14-worker fork pool with jobs `(table_path, seed)` and persisted edge dictionaries under scratch. Every measured command completed in under eight minutes. The official CLI repeats the frozen report configuration only to validate printed means; it is not another selection trial. ## Lookup cost For **one offset** at a known bracket: four signed residual byte reads plus two signed int16 base reads = **8 table bytes**, four sign extensions of int8 and two of int16, four base additions. Bilinear interpolation as three `lerp(a,b,t)=a+(b-a)*t` calls costs 3 subtractions, 3 multiplications and 3 additions. That is **13 arithmetic operations** after f and g are known, excluding address calculation and sign extension. Normalizing both interpolation fractions adds 4 subtractions and 2 divisions if coordinates and knot values are already in the same units. Finding the w bracket by binary search over 81 knots needs at most 7 comparisons/probes, each a uint16 row-index read, plus two endpoint uint16 reads for the final bracket (conservative additional **18 bytes**). Phase 1/2 R bracketing takes at most 6 comparisons over the 49 fixed metadata constants, plus two endpoint accesses; phase 0 has an arithmetic column index. With axis constants compiled into code, they are not table residual reads. The whole ask/bid pair uses **16 table bytes** at the two reflected brackets, with possible sharing of weights and row search because the grid is symmetric. This is an operation count, not an EVM gas measurement. Filter, ln/exp, fee conversion and Solidity fixed-point rounding are additional work. ## Decoder Standalone function, 33 lines, standard library only. Input is the binary bytes obtained by base64 decoding Data. Output can be saved with `json.dump(decode_sym_int8(blob), open('decoded.json','w'))`; unmodified `real.py` loads it. It emits all six original-shape policy arrays, rather than changing real.py's interpolation. ```python def decode_sym_int8(data): """Decode SI81 bytes to the six plain fee tables and their grid metadata.""" import json, struct, bisect if data[:4] != b'SI81': raise ValueError('bad magic') hlen, n = struct.unpack_from('= b for a, b in zip(rows, rows[1:])): raise ValueError('unsorted rows') for ph in range(3): nc = 2*T['FB']+1 if ph == 0 else T['N2'] knots = [] for _ in rows: base = struct.unpack_from('