Quickstart¶
Encode three vectors into a state, save it, load it back, and compare it with a rebuild. After installation, this takes a few minutes and needs no model, network call, or downloaded dataset.
First the whole lifecycle in a dozen lines: encode, save, load, diff. Then the question the floor answers: is a rebuild's change noise, or a real change?
1. Understand the inputs¶
Every example uses the same three embeddings of dimension 4, under the ids
doc-1, doc-2 and doc-3. Each row has unit length, which every operator
requires: 0.6² + 0.8² = 1. The manifest records which encoder produced them.
The examples use quant with four magnitude bins per sign: each coordinate becomes one symbol for its sign and its size.
2. Encode, save, load and diff¶
Use the application directory and run command from your
installation tab. Each program writes corpus.semq into
its working directory, loads it back, and diffs it against a rebuild in which
doc-3 changed.
import numpy as np
from semq import Codec, Encoding
vectors = np.array([
[0.6, 0.8, 0.0, 0.0],
[0.0, 0.6, 0.8, 0.0],
[0.0, 0.0, 0.6, 0.8],
], dtype=np.float32)
ids = ["doc-1", "doc-2", "doc-3"]
manifest = {"encoder": "example-encoder", "encoder_revision": "1"}
codec = Codec.quant(dim=4, bins=4)
codec.encode(vectors, ids=ids, manifest=manifest).save("corpus.semq")
reference = Encoding.load("corpus.semq")
print(reference)
rebuilt = vectors.copy()
rebuilt[2] = [0.8, 0.0, 0.0, 0.6] # doc-3 changed
print(reference.diff(codec.encode(rebuilt, ids=ids, manifest=manifest)))
use semq::{Codec, Encoding, Ids, Manifest};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let vectors: [f32; 12] = [
0.6, 0.8, 0.0, 0.0,
0.0, 0.6, 0.8, 0.0,
0.0, 0.0, 0.6, 0.8,
];
let ids = Ids::utf8(&["doc-1", "doc-2", "doc-3"]);
let manifest = Manifest::from([
("encoder".to_string(), "example-encoder".to_string()),
("encoder_revision".to_string(), "1".to_string()),
]);
let codec = Codec::quant(4, 4)?;
let state = codec.encode(ids, &vectors, Some(&manifest))?;
std::fs::write("corpus.semq", state.to_bytes())?;
let reference = Encoding::from_bytes(&std::fs::read("corpus.semq")?)?;
println!("{reference}");
let mut rebuilt = vectors;
rebuilt[8..].copy_from_slice(&[0.8, 0.0, 0.0, 0.6]); // doc-3 changed
println!("{}", reference.diff(&codec.encode(ids, &rebuilt, Some(&manifest))?)?);
Ok(())
}
package main
import (
"fmt"
"os"
semq "github.com/The-SEMQ-Group/semq/bindings/go"
)
func check(err error) { if err != nil { panic(err) } }
func main() {
vectors := []float32{
0.6, 0.8, 0.0, 0.0,
0.0, 0.6, 0.8, 0.0,
0.0, 0.0, 0.6, 0.8,
}
ids := semq.UTF8IDs("doc-1", "doc-2", "doc-3")
manifest := map[string]string{"encoder": "example-encoder", "encoder_revision": "1"}
codec, err := semq.NewQuantCodec(4, 4)
check(err)
defer codec.Close()
state, err := codec.Encode(ids, vectors, manifest)
check(err)
check(os.WriteFile("corpus.semq", state.Bytes(), 0o644))
state.Close()
data, err := os.ReadFile("corpus.semq")
check(err)
reference, err := semq.Load(data)
check(err)
defer reference.Close()
fmt.Println(reference)
rebuilt := append([]float32(nil), vectors...)
copy(rebuilt[8:], []float32{0.8, 0.0, 0.0, 0.6}) // doc-3 changed
candidate, err := codec.Encode(ids, rebuilt, manifest)
check(err)
defer candidate.Close()
diff, err := reference.Diff(candidate)
check(err)
defer diff.Close()
fmt.Println(diff)
}
import { readFileSync, writeFileSync } from "node:fs";
import { Codec, Encoding } from "@semq/sdk";
const vectors = new Float32Array([
0.6, 0.8, 0.0, 0.0,
0.0, 0.6, 0.8, 0.0,
0.0, 0.0, 0.6, 0.8,
]);
const ids = ["doc-1", "doc-2", "doc-3"];
const manifest = { encoder: "example-encoder", encoder_revision: "1" };
const codec = await Codec.quant(4, 4);
writeFileSync("corpus.semq", codec.encode({ ids, vectors, manifest }).toBytes());
const reference = Encoding.fromBytes(readFileSync("corpus.semq"));
console.log(String(reference));
const rebuilt = vectors.slice();
rebuilt.set([0.8, 0.0, 0.0, 0.6], 8); // doc-3 changed
console.log(String(reference.diff(codec.encode({ ids, vectors: rebuilt, manifest }))));
Every program prints:
Encoding(3 rows, quant(dim=4, bins=4), state_id 6e6f8a89bae4...)
1 of 3 rows changed: doc-3 (hamming 3). 0 added, 0 removed.
The state loaded from corpus.semq has the identity it was saved with. The
diff names the row that changed and its hamming distance: three of the four
symbols of doc-3 differ.
3. Tell rebuild noise from a real change¶
Re-running an encoder on the same corpus moves coordinates a little. A
null rebuild is that: the unchanged corpus through the same encoder again.
The program below measures a floor from three null rebuilds, then checks two
candidates against it: another noisy rebuild, and the rebuild in which doc-3
changed. The numbered markers explain the stand-in for the encoder, the floor
and the gate.
import numpy as np
from semq import Codec, Floor
vectors = np.array([
[0.6, 0.8, 0.0, 0.0],
[0.0, 0.6, 0.8, 0.0],
[0.0, 0.0, 0.6, 0.8],
], dtype=np.float32)
ids = ["doc-1", "doc-2", "doc-3"]
manifest = {"encoder": "example-encoder", "encoder_revision": "1"}
def rebuild(vectors, seed): # (1)!
noise = 1e-3 * np.sin(seed + np.arange(vectors.size)).reshape(vectors.shape)
noisy = (vectors + noise).astype(np.float32)
return noisy / np.linalg.norm(noisy, axis=1, keepdims=True)
codec = Codec.quant(dim=4, bins=4)
reference = codec.encode(vectors, ids=ids, manifest=manifest)
def compare(candidate):
return reference.diff(codec.encode(candidate, ids=ids, manifest=manifest))
floor = Floor.measure([compare(rebuild(vectors, seed)) for seed in (1, 2, 3)]) # (2)!
print(floor)
noisy = compare(rebuild(vectors, 4))
print(noisy)
print(noisy.within(floor)) # (3)!
edited = vectors.copy()
edited[2] = [0.8, 0.0, 0.0, 0.6] # doc-3 changed
changed = compare(edited)
print(changed)
print(changed.within(floor))
- Stands in for re-running your encoder, with about
1e-3of repeatable noise. It is not part of SEMQ. In your pipeline, a null rebuild is the unchanged corpus through the same encoder again. - The floor is the envelope of the three null rebuilds: the worst ratio of changed rows and the largest hamming distance any of them produced, bound to this reference, config and id kind.
- The gate. A candidate is within the floor when it removes no rows,
changes no more of the shared rows than the floor's ratio, stays within
its hamming bound, and keeps
encoderandencoder_revision.
use semq::{Codec, Diff, Floor, Ids, Manifest};
fn rebuild(vectors: &[f32], seed: usize) -> Vec<f32> { // (1)!
let mut noisy: Vec<f32> = vectors
.iter()
.enumerate()
.map(|(i, &x)| (f64::from(x) + 1e-3 * ((seed + i) as f64).sin()) as f32)
.collect();
for row in noisy.chunks_mut(4) {
let norm = row.iter().map(|x| x * x).sum::<f32>().sqrt();
row.iter_mut().for_each(|x| *x /= norm);
}
noisy
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let vectors: [f32; 12] = [
0.6, 0.8, 0.0, 0.0,
0.0, 0.6, 0.8, 0.0,
0.0, 0.0, 0.6, 0.8,
];
let ids = Ids::utf8(&["doc-1", "doc-2", "doc-3"]);
let manifest = Manifest::from([
("encoder".to_string(), "example-encoder".to_string()),
("encoder_revision".to_string(), "1".to_string()),
]);
let codec = Codec::quant(4, 4)?;
let reference = codec.encode(ids, &vectors, Some(&manifest))?;
let compare = |candidate: &[f32]| -> semq::Result<Diff> {
reference.diff(&codec.encode(ids, candidate, Some(&manifest))?)
};
let nulls = (1..=3)
.map(|seed| compare(&rebuild(&vectors, seed)))
.collect::<semq::Result<Vec<_>>>()?;
let floor = Floor::measure(&nulls)?; // (2)!
println!("{floor}");
let noisy = compare(&rebuild(&vectors, 4))?;
println!("{noisy}");
println!("{}", noisy.within(&floor)?); // (3)!
let mut edited = vectors;
edited[8..].copy_from_slice(&[0.8, 0.0, 0.0, 0.6]); // doc-3 changed
let changed = compare(&edited)?;
println!("{changed}");
println!("{}", changed.within(&floor)?);
Ok(())
}
- Stands in for re-running your encoder, with about
1e-3of repeatable noise. It is not part of SEMQ. In your pipeline, a null rebuild is the unchanged corpus through the same encoder again. - The floor is the envelope of the three null rebuilds: the worst ratio of changed rows and the largest hamming distance any of them produced, bound to this reference, config and id kind.
- The gate. A candidate is within the floor when it removes no rows,
changes no more of the shared rows than the floor's ratio, stays within
its hamming bound, and keeps
encoderandencoder_revision.
package main
import (
"fmt"
"math"
semq "github.com/The-SEMQ-Group/semq/bindings/go"
)
func check(err error) { if err != nil { panic(err) } }
func rebuild(vectors []float32, seed int) []float32 { // (1)!
noisy := make([]float32, len(vectors))
for i, x := range vectors {
noisy[i] = float32(float64(x) + 1e-3*math.Sin(float64(seed+i)))
}
for r := 0; r < len(noisy); r += 4 {
var sum float32
for _, x := range noisy[r : r+4] { sum += x * x }
norm := float32(math.Sqrt(float64(sum)))
for c := r; c < r+4; c++ { noisy[c] /= norm }
}
return noisy
}
func main() {
vectors := []float32{
0.6, 0.8, 0.0, 0.0,
0.0, 0.6, 0.8, 0.0,
0.0, 0.0, 0.6, 0.8,
}
ids := semq.UTF8IDs("doc-1", "doc-2", "doc-3")
manifest := map[string]string{"encoder": "example-encoder", "encoder_revision": "1"}
codec, err := semq.NewQuantCodec(4, 4)
check(err)
defer codec.Close()
reference, err := codec.Encode(ids, vectors, manifest)
check(err)
defer reference.Close()
compare := func(candidate []float32) *semq.Diff {
encoded, err := codec.Encode(ids, candidate, manifest)
check(err)
defer encoded.Close() // the Diff keeps the rows it needs
diff, err := reference.Diff(encoded)
check(err)
return diff
}
var nulls []*semq.Diff
for seed := 1; seed <= 3; seed++ {
nulls = append(nulls, compare(rebuild(vectors, seed)))
}
floor, err := semq.MeasureFloor(nulls) // (2)!
check(err)
fmt.Println(floor)
noisy := compare(rebuild(vectors, 4))
fmt.Println(noisy)
within, err := noisy.Within(floor) // (3)!
check(err)
fmt.Println(within)
edited := append([]float32(nil), vectors...)
copy(edited[8:], []float32{0.8, 0.0, 0.0, 0.6}) // doc-3 changed
changed := compare(edited)
fmt.Println(changed)
within, err = changed.Within(floor)
check(err)
fmt.Println(within)
}
- Stands in for re-running your encoder, with about
1e-3of repeatable noise. It is not part of SEMQ. In your pipeline, a null rebuild is the unchanged corpus through the same encoder again. - The floor is the envelope of the three null rebuilds: the worst ratio of changed rows and the largest hamming distance any of them produced, bound to this reference, config and id kind.
- The gate. A candidate is within the floor when it removes no rows,
changes no more of the shared rows than the floor's ratio, stays within
its hamming bound, and keeps
encoderandencoder_revision.
import { Codec, Floor } from "@semq/sdk";
const vectors = new Float32Array([
0.6, 0.8, 0.0, 0.0,
0.0, 0.6, 0.8, 0.0,
0.0, 0.0, 0.6, 0.8,
]);
const ids = ["doc-1", "doc-2", "doc-3"];
const manifest = { encoder: "example-encoder", encoder_revision: "1" };
function rebuild(vectors: Float32Array, seed: number): Float32Array { // (1)!
const noisy = vectors.map((x, i) => x + 1e-3 * Math.sin(seed + i));
for (let r = 0; r < noisy.length; r += 4) {
const row = noisy.subarray(r, r + 4);
const norm = Math.hypot(...row);
row.forEach((x, c) => { row[c] = x / norm; });
}
return noisy;
}
const codec = await Codec.quant(4, 4);
const reference = codec.encode({ ids, vectors, manifest });
const compare = (candidate: Float32Array) =>
reference.diff(codec.encode({ ids, vectors: candidate, manifest }));
const floor = Floor.measure([1, 2, 3].map((seed) => compare(rebuild(vectors, seed)))); // (2)!
console.log(String(floor));
const noisy = compare(rebuild(vectors, 4));
console.log(String(noisy));
console.log(noisy.within(floor)); // (3)!
const edited = vectors.slice();
edited.set([0.8, 0.0, 0.0, 0.6], 8); // doc-3 changed
const changed = compare(edited);
console.log(String(changed));
console.log(changed.within(floor));
- Stands in for re-running your encoder, with about
1e-3of repeatable noise. It is not part of SEMQ. In your pipeline, a null rebuild is the unchanged corpus through the same encoder again. - The floor is the envelope of the three null rebuilds: the worst ratio of changed rows and the largest hamming distance any of them produced, bound to this reference, config and id kind.
- The gate. A candidate is within the floor when it removes no rows,
changes no more of the shared rows than the floor's ratio, stays within
its hamming bound, and keeps
encoderandencoder_revision.
4. Check the result¶
Every program prints the following; Python writes the two booleans as True
and False.
Floor(3 of 3 rows, hamming 2, from 3 nulls)
3 of 3 rows changed: doc-1 (hamming 1), doc-2 (hamming 1), doc-3 (hamming 1). 0 added, 0 removed.
true
1 of 3 rows changed: doc-3 (hamming 3). 0 added, 0 removed.
false
The zeros in these vectors sit exactly on the sign boundary, so the noise of
a rebuild flips some of their symbols: across the three null rebuilds every
row changed, by at most two symbols. That is the floor. The noisy candidate
stays within it and passes. The changed doc-3 moves three symbols, more than
any null rebuild did, so it fails. Real embeddings rarely sit exactly on a
boundary; these do so that a dozen numbers show what rebuild noise looks like.
For the full machine-readable report, call diff.as_dict() in Python or run
semq diff --json --floor FLOOR.json; see the
report schema and the
CLI reference.
The same float32 input, ids and manifest produce the same state_id in every
language and on every supported architecture. The shared
conformance vectors
pin the bytes and verdicts every binding is checked against.
From a shell, the Python package installs the semq command:
semq show corpus.semq
Next steps¶
- Gate a rebuild: measure a floor from null rebuilds and gate candidates with the CLI.
- Choose a configuration: operators, bins, sectors and scale.
- Concepts and architecture: states, identities and what a diff can tell you.
- API reference: every public operation.