Line data Source code
1 : // Copyright 2026 The Authors.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'dart:math';
16 : import 'dart:typed_data';
17 :
18 : /// Quantises a float32 embedding vector to unsigned 8-bit integers (SQ8).
19 : ///
20 : /// Uses a **fixed symmetric range** suitable for L2-normalised vectors whose
21 : /// components lie in `[-1.0, 1.0]`:
22 : ///
23 : /// ```
24 : /// u = clamp(round((f + 1.0) / 2.0 * 255), 0, 255)
25 : /// ```
26 : ///
27 : /// This maps `-1.0` → `0`, `0.0` → `127` (or `128`), `1.0` → `255`.
28 : ///
29 : /// ## Assumptions
30 : ///
31 : /// - Input must be an L2-normalised vector (all components in `[-1.0, 1.0]`).
32 : /// Values marginally outside the range (e.g. due to float rounding) are
33 : /// clamped rather than panicked.
34 : /// - No per-vector calibration is required; the fixed range provides adequate
35 : /// accuracy for cosine similarity ranking.
36 : /// - [vector] may have any positive length (384 for BGE Small En v1.5, 1024
37 : /// for BGE-M3, etc.). A debug-mode assertion fires for empty vectors.
38 : ///
39 : /// The quantisation error is bounded by `2.0 / 255 ≈ 0.00784` per component
40 : /// (one quantisation step). In practice, round-trip error is ≤ 0.004 per
41 : /// element.
42 1 : Uint8List quantise(Float32List vector) {
43 : assert(
44 2 : vector.isNotEmpty,
45 2 : 'Embedding vector must be non-empty, got length ${vector.length}',
46 : );
47 2 : final out = Uint8List(vector.length);
48 3 : for (var i = 0; i < vector.length; i++) {
49 1 : final f = vector[i];
50 : // Map [-1, 1] → [0, 255] and clamp to guard against float rounding.
51 4 : final u = ((f + 1.0) / 2.0 * 255.0).roundToDouble();
52 4 : out[i] = min(255, max(0, u.toInt()));
53 : }
54 : return out;
55 : }
56 :
57 : /// Dequantises an SQ8-encoded vector back to float32.
58 : ///
59 : /// Inverse of [quantise]:
60 : ///
61 : /// ```
62 : /// f = u / 255.0 * 2.0 - 1.0
63 : /// ```
64 : ///
65 : /// The reconstructed values are in `[-1.0, 1.0]` but are no longer
66 : /// L2-normalised (the quantisation error means the norm is slightly off 1.0).
67 : /// For cosine similarity via dot product this is acceptable — the ranking
68 : /// order is preserved.
69 : ///
70 : /// [vector] may have any positive length. A debug-mode assertion fires for
71 : /// empty vectors.
72 1 : Float32List dequantise(Uint8List vector) {
73 : assert(
74 2 : vector.isNotEmpty,
75 2 : 'SQ8 vector must be non-empty, got length ${vector.length}',
76 : );
77 2 : final out = Float32List(vector.length);
78 3 : for (var i = 0; i < vector.length; i++) {
79 5 : out[i] = vector[i] / 255.0 * 2.0 - 1.0;
80 : }
81 : return out;
82 : }
|