Phasor docs
Phasor docs
Task-oriented notes with complete programs. Each example was compiled and run against the published package, and the output shown is what it printed.
Reading a Pleroma
A store is built item by item with Add, which takes a signed count, and Remove erases one copy. The key view keeps exactly the items with distinct keys up to KeyCapacity and refuses past that. Every other view reads its own slice of the array and stays exact at any load, within the field's range.
TryKeys returns the key, total count and the flag word, arrangement or tick when there is one per key. TryItems returns whole items when each key has one value per face factor. Both return false past capacity, never a wrong answer.
- Flags, Near, Nearest
- Flags returns the exact count at every flag word. Near(x, r) counts items whose flags differ from x in at most r bits, and Nearest(x) returns the nearest stored words with their distances.
- Arrangements, SignBalance
- Counts per arrangement of up to 5 things, and the even-minus-odd balance.
- Times, Tick, Diffuse
- Counts per tick. Tick ages every item, and Diffuse runs a lazy random walk over time on every item at once. Time views then return TimeScale times the exact expected counts.
- JointCounts
- Set JointFaces, for example Flags and Time, and JointCounts returns the exact joint counts. The joint slice holds the product of the faces' sizes, so size it deliberately.
- CountWhere, Compile
- Count the items whose flag word satisfies any predicate with one inner product. Compile prepares a predicate for repeated queries, and CountWhereByTime and CountWhereByArrangement give the count per tick or per arrangement.
- BitCounts, FlagParity, TryFindConflicts
- Per-bit counts and the XOR of every stored word from a few cells, and the keys that carry more than one flag word or arrangement.
Bind two stores
store.Bind(other) binds every pair of items at once: keys add, flag words XOR, arrangements compose, ticks add, and counts multiply. The cost is one product per order representation, not per pair of items. Binding with a one-item store shifts the whole store on every face together, and binding with a larger store applies the mix of all those shifts.
using Phasor;
var shape = new HologramShape { KeyCapacity = 8, FlagBits = 4, TimePeriod = 9 };
var store = new Pleroma(shape);
store.Add(new HologramItem(Key: 10, Flags: 0b0011, Time: 2), 3);
store.Add(new HologramItem(Key: 20, Flags: 0b0111, Time: 5), 1);
// Bind with a one-item store: every item moves by the same key, flags and time shift.
var shift = new Pleroma(shape);
shift.Add(new HologramItem(Key: 1, Flags: 0b0001, Time: 1));
store.Bind(shift);
if (store.TryItems(out var items))
foreach (var (item, count) in items)
Console.WriteLine($"{item.Key}: flags {item.Flags}, time {item.Time}, count {count}");11: flags 2, time 3, count 3
21: flags 6, time 6, count 1Apply a weighted kernel in one pass
A PleromaKernel is a weighted sum of any number of flag flips, arrangement relabellings and time shifts, held as one harmonic factor per stored harmonic. store.Apply(kernel) gives exactly the store you would get by moving every item by every element and adding the copies, at a cost that does not depend on how many elements the kernel holds. Kernels add and compose, and Diffuse is the two-element case.
The example applies one identity plus two copies of an item that has a flag flipped and a tick added, to three items. Every flag and time count changes at once.
using Phasor;
var store = new Pleroma(new HologramShape
{
KeyCapacity = 4,
FlagBits = 3,
TimePeriod = 7,
JointFaces = HologramFaces.Flags | HologramFaces.Time,
});
store.Add(new HologramItem(Key: 10, Flags: 0b001, Time: 0), 3);
store.Add(new HologramItem(Key: 20, Flags: 0b010, Time: 2), 2);
store.Add(new HologramItem(Key: 30, Flags: 0b100, Time: 4), 1);
Console.WriteLine("before flags " + string.Join(" ", store.Flags()));
Console.WriteLine("before times " + string.Join(" ", store.Times()));
// One kernel: 1x identity plus 2x (flip flag bit 0 and age one tick), applied to every item in one pass.
var kernel = new PleromaKernel(store.Shape);
kernel.Add(weight: 1L);
kernel.Add(flagMask: 0b001, timeShift: 1, weight: 2L);
store.Apply(kernel);
Console.WriteLine("after flags " + string.Join(" ", store.Flags()));
Console.WriteLine("after times " + string.Join(" ", store.Times()));before flags 0 3 2 0 1 0 0 0
before times 3 0 2 0 1 0 0
after flags 6 3 2 4 1 2 0 0
after times 3 6 2 4 1 2 0Bit counts and parity
BitCounts gives the exact count of items with each bit set, and FlagParity gives the XOR of every stored word, both from a handful of cells.
using Phasor;
var store = new Pleroma(new HologramShape { FlagBits = 4, KeyCapacity = 8 });
store.Add(new HologramItem(Key: 1, Flags: 0b0011), 2); // two copies
store.Add(new HologramItem(Key: 1, Flags: 0b0110), 1); // one copy
store.Add(new HologramItem(Key: 2, Flags: 0b0001), 3); // three copies
long[] bitCounts = store.BitCounts(); // exact count of items with each bit set
uint parity = store.FlagParity(); // XOR of every stored word
Console.WriteLine($"{bitCounts[0]} {bitCounts[1]} {bitCounts[2]} {bitCounts[3]} parity {parity}");5 3 1 0 parity 7More operations
- ScaleKeys, CombineCycles
- With CompleteKeys a store keeps every harmonic of a small key cycle. ScaleKeys(u) relabels every key a to u a in place, and CombineCycles joins two stores on coprime cycles into one on their product, by the Chinese remainder theorem.
- Frobenius, ShiftRegister
- On a store built without per-bit item lines, Frobenius squares every flag word in GF(2^m) and ShiftRegister steps every word through a linear shift register, exactly, any number of steps in one pass.
- Syndrome, IsIndependent, Sample
- Count stored words per Hamming syndrome, test in exact integers whether joint counts are the product of their marginals, and draw one value of any view with probability count over total.
- RelationFace, ChargeLattice
- Store relations between a small set of keys and read a loop's winding, its frustration parity and the direction of its net circulation. Integer offsets on a periodic lattice give each cube an exact charge.
- Eliminate
- With one flag bit as a label, every stored word is an example, and Eliminate solves for the parity rule over GF(2) that fits every example, or reports that none does.
- Dense pages
- With CompleteKeys a store is a page of up to 4,096 slots in which every slot reads back exactly. FromCounts and AddCounts build a page from a count vector in one transform, CountWhereKey sums any function of the key in one read, and InnerProduct gives the exact equi-join sum of two pages.
- Sparse pages
- PleromaSparsePage keeps exact counts over keys anywhere in a huge key set in memory proportional to the keys present, with exact merges and draws.
- Redundancy
- ReservedKeys with Verify and Repair detect and rebuild lost harmonics, CheckPrime and VerifyCheck detect corruption of either copy, and PleromaParity rebuilds any one lost store of a group.
- SQL-style compositions
- Restrict, Support (and so COUNT DISTINCT), MapCounts, sums per group and joins are exact, built from the operations above.
CycleHologram: the keys-only case
CycleHologram is the Pleroma with keys only, kept as its own type. The phasor bundle, a sum of phasors, is a hologram: many items superposed in one fixed-size vector, read out approximately, with crosstalk that grows with load. CycleHologram is the same superposition on the discrete cycle of a finite field. Each symbol is an element of the cycle, the hologram holds power sums of the stored elements, and the readout is algebraic instead of statistical, so there is no similarity threshold and no rounding.
A hologram of capacity k holds 2k+1 eight-byte elements, and TryDecode returns every stored symbol with its signed count when at most k distinct symbols are stored. With k+1 distinct symbols decode always refuses. Every decode is re-encoded and must reproduce every stored element. Holograms add as a multiset union, subtract as an exact signed difference, and serialize with their field metadata.
Each replica in the second example holds 10,000 items and the hologram has capacity 4, so it finds what differs by exchanging one small hologram sized to the difference, not to the data. If the replicas differ in more than the capacity, TryDecode returns false, so size the capacity to the largest difference you expect.
The decoder is the syndrome decoder of Reed-Solomon and BCH codes (Berlekamp-Massey), the construction behind the PinSketch set sketch and the minisketch library.
using Phasor;
var stock = new CycleHologram(capacity: 8);
stock.Add(CycleHologram.Symbol("apples"), 10);
stock.Add(42, 5); // integer symbols are exact
stock.Add(CycleHologram.Symbol("oranges"), 3);
if (stock.TryDecode(out var items))
{
foreach (var (symbol, count) in items.OrderBy(i => i.Item2))
Console.WriteLine($"count {count}"); // every count is exact
}count 3
count 5
count 10using Phasor;
var a = new CycleHologram(capacity: 4);
var b = new CycleHologram(capacity: 4);
for (ulong id = 0; id < 10_000; id++) { a.Add(id); b.Add(id); }
a.Add(777_001); // only on A
b.Add(777_002, 2); // only on B, two copies
byte[] wire = a.Serialize(); // what A sends
Console.WriteLine($"wire size {wire.Length} bytes");
var delta = CycleHologram.Deserialize(wire);
delta.Subtract(b); // B subtracts its own hologram: A minus B
if (delta.TryDecode(out var diff))
foreach (var (symbol, count) in diff.OrderBy(d => d.Item1))
Console.WriteLine($"{symbol}: {count}");wire size 192 bytes
777001: 1
777002: -2The phasor codecs
Phasor gives you two operations. Encode turns a number into a face with its value as phase, and gives a symbol a salted hash-based random signature. Compose with bind (complex multiply per component) to build relationships, bundle (sum) to collect sets, and unbind (conjugate) to extract parts, and read out with correlation. On the linear band of a face, bind is addition. On the log band it is multiplication. There is no separate calculator: arithmetic is the composition.
PhasorCodec has a fixed width of 256 reals, with a frozen identity prefix and a learned orbital tail, and is built for neural models. HoloCodec has a configurable width, uses SIMD, has a planar layout and a memoized number cache, and is built for holographic storage and similarity search at scale. Do not mix faces between the two.
using Phasor;
// Encode a number and some symbols
double[] five = PhasorCodec.Encode("5");
double[] three = PhasorCodec.Encode("3");
double[] age = PhasorCodec.Encode("age");
double[] name = PhasorCodec.Encode("name");
double[] hello = PhasorCodec.Encode("hello");
// Bind two numbers: adds them on the linear band
double[] fivePlusThree = PhasorCodec.Bind(five, three);
Console.WriteLine($"5 + 3 = {PhasorCodec.DecodeSum(fivePlusThree, max: 10)}");
// Bind each value to a role, bundle the pairs into one record, then unbind a role to read its value back
double[] record = PhasorCodec.Bundle(PhasorCodec.Bind(age, five), PhasorCodec.Bind(name, hello));
double[] ageField = PhasorCodec.Unbind(record, age);
Console.WriteLine($"age field matches 5 better than 3: {PhasorCodec.Correlate(ageField, five) > PhasorCodec.Correlate(ageField, three)}");
// For larger workloads and similarity search, HoloCodec is SIMD and configurable in width
var codec = new HoloCodec(dim: 1024);
double[] encoded = codec.Encode("hello");
Console.WriteLine($"HoloCodec self-similarity is positive: {HoloCodec.Dot(encoded, codec.Encode("hello")) > 0}");5 + 3 = 8
age field matches 5 better than 3: True
HoloCodec self-similarity is positive: TrueHologram: readable algebra with operators
Hologram wraps a phasor face with operators for readability and prototyping. It is a readonly struct around a double array, so it converts freely to and from arrays without copying.
Bind is the operator *, per-component complex multiply. Bundle is +, a component-wise sum that merges holograms into one superposition. Unbind is /, multiplying by the conjugate. Correlate is a method that gives a dot product, and ~ flips the sign of the imaginary parts. Readout by correlation is approximate. For an exact readout use Pleroma, or CycleHologram for keys only.
Read those four numbers carefully, because the two fields behave differently. The name field separates completely: 128.59 for the right value against -1.96 for the wrong one. Two symbols get independent random signatures, so an unrelated symbol correlates at the noise floor, near zero.
The age field separates by less than 3x: 32.59 for 7 against 13.39 for 5. That is the codec telling the truth. A number is encoded as a phase proportional to its value, so 5 and 7 are similar faces and 5 and 500 are not. Numbers carry a metric and symbols do not. If you want numbers to behave like symbols, encode them with FromSymbol instead of FromNumber, and give up arithmetic to get it.
So correlation separates signal from noise for symbols, and separates signal from nearby signal only by distance for numbers. Design the readout accordingly: take an argmax over candidate values rather than a threshold, which is what DecodeSum and DecodeProduct do.
using Phasor;
var dim = 256; // Hologram default dimension
// A record is a superposition of role-value pairs
var nameRole = Hologram.FromSymbol("name", dim);
var lily = Hologram.FromSymbol("Lily", dim);
var ageRole = Hologram.FromSymbol("age", dim);
var seven = Hologram.FromNumber(7, dim);
var record = Hologram.Zero(dim) + nameRole * lily + ageRole * seven; // bind, then bundle
// Unbind a role and correlate with candidate values
var nameField = record / nameRole;
var ageField = record / ageRole;
Console.WriteLine($"Name field has Lily: {nameField.Correlate(lily):F2}");
Console.WriteLine($"Name field has John: {nameField.Correlate(Hologram.FromSymbol("John", dim)):F2}");
Console.WriteLine($"Age field has 7: {ageField.Correlate(seven):F2}");
Console.WriteLine($"Age field has 5: {ageField.Correlate(Hologram.FromNumber(5, dim)):F2}");Name field has Lily: 128.59
Name field has John: -1.96
Age field has 7: 32.59
Age field has 5: 13.39Arithmetic with numbers
When you bind two numbers their bands compose. The face carries both operations at once, and you read out the band you care about. Arithmetic is composition, not calculation.
The operators allocate a new array per operation. In tight loops or accumulation, use the in-place static methods BindInto, BundleInto and UnbindInto with a buffer you allocate once. At typical dimensions the allocating Bind operator is about 1.7 times slower than BindInto.
using Phasor;
var dim = 256;
// Linear band: binding two numbers adds them
var sumEncoded = Hologram.FromNumber(3, dim) * Hologram.FromNumber(5, dim);
Console.WriteLine(PhasorCodec.DecodeSum(sumEncoded, max: 20));
// Log band: binding two numbers multiplies them
var productEncoded = Hologram.FromNumber(2, dim) * Hologram.FromNumber(4, dim);
Console.WriteLine(PhasorCodec.DecodeProduct(productEncoded, max: 16));
// In a loop, use the in-place methods: they reuse one buffer instead of allocating per call
double[] accumulator = new double[dim];
for (int i = 0; i < 100; i++)
Hologram.BundleInto(accumulator, Hologram.FromNumber(i, dim).Reals);
Console.WriteLine($"accumulated {accumulator.Length} components");8
8
accumulated 256 componentsThings to know
- Layouts do not mix
- Hologram uses the interleaved layout, the same as PhasorCodec. HoloCodec is planar, with all reals then all imaginary parts, and produces garbage if given an interleaved face.
- Dimension floor
- PhasorCodec requires an even dimension of at least 128: 64 frozen reals and at least 64 orbital. Odd dimensions align down, and anything below 128 throws an ArgumentException.
- .Reals is live
- The Reals property returns the internal array, not a copy, so changing it changes the hologram. Use it read-only or pass it to the in-place methods.
- Small fields wrap
- Every view's count must lie within half the field per prime. On a small test field, large kernel weights wrap, so compare cells there instead of counts.