Skip to content

Commit e81ee6d

Browse files
committed
feat: Implement bitwise operations for Z3, Z7, and ZORDER indices; add helper functions for digit manipulation and extraction
- Introduced fixed bit-position encoding for Z3 and Z7 in `index-arithmetic.md`. - Added helper functions for getting and setting digits in Z3 and Z7 indices in `webdggrid.ts`. - Created unit tests for Z3, Z7, and ZORDER digit operations, ensuring consistency with SEQNUM round-trip conversions.
1 parent 83a37ad commit e81ee6d

File tree

6 files changed

+1034
-240
lines changed

6 files changed

+1034
-240
lines changed

docs/.vitepress/theme/components/DggsAddressTypesDemo.vue

Lines changed: 169 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -111,105 +111,120 @@ function verifyRoundTrip(addr, resolution) {
111111
return checks
112112
}
113113
114-
// --- Bitarithmetic demonstration ---
114+
// --- Hierarchical index demonstration ---
115+
//
116+
// Z7 and Z3 use FIXED BIT-POSITION encoding (NOT simple positional radix).
117+
// Layout (64-bit):
118+
// Z7: bits[63:60] = quad (4 bits), then 20 x 3-bit digits (res 1..20)
119+
// Z3: bits[63:60] = quad (4 bits), then 30 x 2-bit digits (res 1..30)
120+
//
121+
// Rather than trying to replicate DGGRID's internal conventions, we use the
122+
// API for parent/children and then OBSERVE the digit patterns to show how
123+
// the encoding works.
115124
116-
function computeBitOps(addr, resolution) {
117-
const ops = { sections: [] }
125+
const Z7_BITS = 3n
126+
const Z7_MAX = 20
127+
const Z3_BITS = 2n
128+
const Z3_MAX = 30
118129
119-
// Z3 arithmetic (aperture 3)
120-
if (addr.z3 !== null) {
121-
const z3 = addr.z3
122-
const base = 3n
123-
// Extract digits: each resolution level is one base-3 digit
124-
// The Z3 value encodes digits from MSB (coarsest) to LSB (finest)
125-
const digits = extractDigits(z3, base, resolution)
126-
127-
// Parent via bit manipulation: drop the last digit
128-
const parentZ3Bits = z3 / base
129-
let parentSeqBits = null
130-
try { parentSeqBits = dggs.z3ToSequenceNum(parentZ3Bits, resolution - 1) } catch { /* skip */ }
131-
132-
// Actual parent via API
133-
let parentSeqApi = null
134-
try { parentSeqApi = dggs.sequenceNumParent([addr.seqnum], resolution)[0] } catch { /* skip */ }
135-
136-
// Children via bit manipulation: append digits 0,1,2
137-
const childrenBits = []
138-
for (let d = 0n; d < base; d++) {
139-
const childZ3 = z3 * base + d
140-
try {
141-
const childSeq = dggs.z3ToSequenceNum(childZ3, resolution + 1)
142-
childrenBits.push({ digit: Number(d), z3: childZ3, seqnum: childSeq })
143-
} catch { childrenBits.push({ digit: Number(d), z3: childZ3, seqnum: null }) }
144-
}
130+
function zGetQuad(value) {
131+
return Number((value >> 60n) & 0xFn)
132+
}
145133
146-
// Actual children via API
147-
let childrenApi = []
148-
try { childrenApi = dggs.sequenceNumChildren([addr.seqnum], resolution)[0] } catch { /* skip */ }
134+
function zGetDigit(value, res, bitsPerDigit, maxRes) {
135+
const shift = BigInt(maxRes - res) * bitsPerDigit
136+
const mask = (1n << bitsPerDigit) - 1n
137+
return Number((value >> shift) & mask)
138+
}
149139
150-
ops.sections.push({
151-
type: 'Z3',
152-
base: 3,
153-
value: z3,
154-
hex: '0x' + z3.toString(16),
155-
digits,
156-
parent: {
157-
formula: `z3 / ${base} = ${parentZ3Bits}`,
158-
bitResult: parentSeqBits,
159-
apiResult: parentSeqApi,
160-
match: parentSeqBits !== null && parentSeqApi !== null && parentSeqBits === parentSeqApi,
161-
},
162-
children: childrenBits,
163-
childrenApi,
164-
})
140+
function zExtractDigits(value, resolution, bitsPerDigit, maxRes) {
141+
const digits = []
142+
for (let r = 1; r <= resolution; r++) {
143+
digits.push(zGetDigit(value, r, bitsPerDigit, maxRes))
165144
}
145+
return digits
146+
}
166147
167-
// Z7 arithmetic (aperture 7)
168-
if (addr.z7 !== null) {
169-
const z7 = addr.z7
170-
const base = 7n
171-
const digits = extractDigits(z7, base, resolution)
172-
173-
const parentZ7Bits = z7 / base
174-
let parentSeqBits = null
175-
try { parentSeqBits = dggs.z7ToSequenceNum(parentZ7Bits, resolution - 1) } catch { /* skip */ }
176-
177-
let parentSeqApi = null
178-
try { parentSeqApi = dggs.sequenceNumParent([addr.seqnum], resolution)[0] } catch { /* skip */ }
179-
180-
const childrenBits = []
181-
for (let d = 0n; d < base; d++) {
182-
const childZ7 = z7 * base + d
183-
try {
184-
const childSeq = dggs.z7ToSequenceNum(childZ7, resolution + 1)
185-
childrenBits.push({ digit: Number(d), z7: childZ7, seqnum: childSeq })
186-
} catch { childrenBits.push({ digit: Number(d), z7: childZ7, seqnum: null }) }
148+
function computeZSection(type, value, seqnum, resolution, bitsPerDigit, maxRes, toZ, fromZ) {
149+
const quad = zGetQuad(value)
150+
const digits = zExtractDigits(value, resolution, bitsPerDigit, maxRes)
151+
152+
// Use API for parent, then convert to Z to observe digit change
153+
let parentSeq = null
154+
let parentZ = null
155+
let parentDigits = null
156+
let parentQuad = null
157+
try {
158+
parentSeq = resolution > 0 ? dggs.sequenceNumParent([seqnum], resolution)[0] : null
159+
if (parentSeq !== null) {
160+
parentZ = toZ(parentSeq, resolution - 1)
161+
parentQuad = zGetQuad(parentZ)
162+
parentDigits = zExtractDigits(parentZ, resolution - 1, bitsPerDigit, maxRes)
187163
}
164+
} catch { /* skip */ }
188165
189-
let childrenApi = []
190-
try { childrenApi = dggs.sequenceNumChildren([addr.seqnum], resolution)[0] } catch { /* skip */ }
166+
// Use API for children, then convert each to Z to observe digit patterns
167+
let childrenApi = []
168+
try { childrenApi = dggs.sequenceNumChildren([seqnum], resolution)[0] } catch { /* skip */ }
191169
192-
ops.sections.push({
193-
type: 'Z7',
194-
base: 7,
195-
value: z7,
196-
hex: '0x' + z7.toString(16),
197-
digits,
198-
parent: {
199-
formula: `z7 / ${base} = ${parentZ7Bits}`,
200-
bitResult: parentSeqBits,
201-
apiResult: parentSeqApi,
202-
match: parentSeqBits !== null && parentSeqApi !== null && parentSeqBits === parentSeqApi,
203-
},
204-
children: childrenBits,
205-
childrenApi,
206-
})
170+
const childrenInfo = childrenApi.map(childSeq => {
171+
try {
172+
const childZVal = toZ(childSeq, resolution + 1)
173+
const childDigits = zExtractDigits(childZVal, resolution + 1, bitsPerDigit, maxRes)
174+
return {
175+
seqnum: childSeq,
176+
zValue: childZVal,
177+
hex: '0x' + childZVal.toString(16).padStart(16, '0'),
178+
digits: childDigits,
179+
newDigit: childDigits[resolution], // the digit at the child's resolution level
180+
}
181+
} catch {
182+
return { seqnum: childSeq, zValue: null, hex: 'err', digits: [], newDigit: '?' }
183+
}
184+
})
185+
186+
return {
187+
type,
188+
base: Number(1n << bitsPerDigit),
189+
bitsPerDigit: Number(bitsPerDigit),
190+
value,
191+
hex: '0x' + value.toString(16).padStart(16, '0'),
192+
quad,
193+
digits,
194+
parent: {
195+
seqnum: parentSeq,
196+
zValue: parentZ,
197+
hex: parentZ !== null ? '0x' + parentZ.toString(16).padStart(16, '0') : null,
198+
quad: parentQuad,
199+
digits: parentDigits,
200+
samePrefix: parentDigits !== null && digits.slice(0, -1).every((d, i) => d === parentDigits[i]),
201+
},
202+
children: childrenInfo,
203+
}
204+
}
205+
206+
function computeBitOps(addr, resolution) {
207+
const ops = { sections: [] }
208+
209+
if (addr.z3 !== null) {
210+
ops.sections.push(computeZSection(
211+
'Z3', addr.z3, addr.seqnum, resolution, Z3_BITS, Z3_MAX,
212+
(seq, res) => dggs.sequenceNumToZ3(seq, res),
213+
(z, res) => dggs.z3ToSequenceNum(z, res),
214+
))
215+
}
216+
217+
if (addr.z7 !== null) {
218+
ops.sections.push(computeZSection(
219+
'Z7', addr.z7, addr.seqnum, resolution, Z7_BITS, Z7_MAX,
220+
(seq, res) => dggs.sequenceNumToZ7(seq, res),
221+
(z, res) => dggs.z7ToSequenceNum(z, res),
222+
))
207223
}
208224
209225
// ZORDER spatial locality
210226
if (addr.zorder !== null) {
211227
const zorder = addr.zorder
212-
// Show neighbor ZORDER values to demonstrate spatial locality
213228
let neighbors = []
214229
try {
215230
const nIds = dggs.sequenceNumNeighbors([addr.seqnum], resolution)[0]
@@ -229,7 +244,6 @@ function computeBitOps(addr, resolution) {
229244
})
230245
} catch { /* skip */ }
231246
232-
// Common prefix with neighbors (shared ancestor)
233247
const centerHex = zorder.toString(16).padStart(16, '0')
234248
const prefixes = neighbors.filter(n => n.zorder !== null).map(n => {
235249
const nHex = n.zorder.toString(16).padStart(16, '0')
@@ -252,16 +266,6 @@ function computeBitOps(addr, resolution) {
252266
return ops.sections.length > 0 ? ops : null
253267
}
254268
255-
function extractDigits(value, base, numDigits) {
256-
const digits = []
257-
let v = value
258-
for (let i = 0; i < numDigits; i++) {
259-
digits.unshift(Number(v % base))
260-
v = v / base
261-
}
262-
return digits
263-
}
264-
265269
function formatAddress(addr, type) {
266270
if (!addr) return '?'
267271
switch (type) {
@@ -513,76 +517,95 @@ function loadScript(src) {
513517

514518
<div v-for="section in state.bitOps.sections" :key="section.type" class="bitops-section">
515519

516-
<!-- Z3 / Z7 hierarchical arithmetic -->
520+
<!-- Z3 / Z7 hierarchical encoding -->
517521
<template v-if="section.type === 'Z3' || section.type === 'Z7'">
518-
<div class="bitops-title">{{ section.type }} — Base-{{ section.base }} Hierarchical Arithmetic</div>
522+
<div class="bitops-title">{{ section.type }} — Fixed Bit-Position Encoding ({{ section.bitsPerDigit }} bits/digit)</div>
519523

520-
<!-- Digit breakdown -->
524+
<!-- Bit layout of selected cell -->
521525
<div class="digit-row">
522-
<span class="digit-label">{{ section.type }} value:</span>
526+
<span class="digit-label">Selected cell:</span>
523527
<span class="digit-value">{{ section.hex }}</span>
524528
</div>
525529
<div class="digit-row">
526-
<span class="digit-label">Digits (res 1→{{ state.selectedRes }}):</span>
530+
<span class="digit-label">Digit layout:</span>
527531
<span class="digit-cells">
532+
<span class="digit-cell digit-quad" title="Quad (icosahedron face)">Q{{ section.quad }}</span>
528533
<span
529534
v-for="(d, i) in section.digits"
530535
:key="i"
531536
class="digit-cell"
532537
:class="{ 'digit-last': i === section.digits.length - 1 }"
538+
:title="'Res ' + (i + 1) + ' → digit ' + d"
533539
>{{ d }}</span>
534540
</span>
535-
<span class="digit-hint">each digit = which child at that level</span>
541+
<span class="digit-hint">4-bit quad + {{ section.bitsPerDigit }}-bit digits at fixed bit positions</span>
536542
</div>
537543

538-
<!-- Parent via division -->
539-
<div class="op-block">
540-
<div class="op-title">Find Parent (drop last digit)</div>
541-
<div class="op-code">
542-
<code>{{ section.type.toLowerCase() }} / {{ section.base }}</code>
543-
= <code>{{ section.parent.formula.split('=')[1]?.trim() }}</code>
544-
</div>
544+
<!-- Parent — observe the digit pattern -->
545+
<div class="op-block" v-if="section.parent.seqnum !== null">
546+
<div class="op-title">Parent Cell (res {{ state.selectedRes - 1 }})</div>
545547
<div class="op-row">
546-
<span class="op-label">Bit arithmetic result:</span>
547-
<span class="op-val">SEQNUM {{ section.parent.bitResult?.toString() ?? 'error' }}</span>
548+
<span class="op-label">SEQNUM:</span>
549+
<span class="op-val">{{ section.parent.seqnum.toString() }}</span>
548550
</div>
549551
<div class="op-row">
550-
<span class="op-label">API sequenceNumParent():</span>
551-
<span class="op-val">SEQNUM {{ section.parent.apiResult?.toString() ?? 'error' }}</span>
552+
<span class="op-label">{{ section.type }}:</span>
553+
<span class="op-val">{{ section.parent.hex }}</span>
552554
</div>
553-
<div class="op-row">
554-
<span class="op-label">Match:</span>
555-
<span :class="section.parent.match ? 'rt-ok' : 'rt-fail'">
556-
{{ section.parent.match ? 'Yes — identical!' : 'No' }}
555+
<div class="digit-row" style="margin-top: 4px">
556+
<span class="digit-label">Parent digits:</span>
557+
<span class="digit-cells">
558+
<span class="digit-cell digit-quad">Q{{ section.parent.quad }}</span>
559+
<span
560+
v-for="(d, i) in section.parent.digits"
561+
:key="i"
562+
class="digit-cell"
563+
:class="{ 'digit-match': i < section.digits.length - 1 && d === section.digits[i] }"
564+
>{{ d }}</span>
565+
</span>
566+
</div>
567+
<div class="digit-row">
568+
<span class="digit-label">Selected digits:</span>
569+
<span class="digit-cells">
570+
<span class="digit-cell digit-quad">Q{{ section.quad }}</span>
571+
<span
572+
v-for="(d, i) in section.digits"
573+
:key="i"
574+
class="digit-cell"
575+
:class="{ 'digit-match': i < section.digits.length - 1 && section.parent.digits && d === section.parent.digits[i], 'digit-last': i === section.digits.length - 1 }"
576+
>{{ d }}</span>
557577
</span>
558578
</div>
579+
<p class="op-hint" v-if="section.parent.samePrefix">
580+
The parent shares digits 1–{{ state.selectedRes - 1 }} with the selected cell. Only the last digit differs — it encodes which child this cell is within its parent.
581+
</p>
559582
</div>
560583

561-
<!-- Children via multiplication -->
562-
<div class="op-block">
563-
<div class="op-title">Find Children (append digit 0–{{ section.base - 1 }})</div>
584+
<!-- Children — observe how each gets a unique digit -->
585+
<div class="op-block" v-if="section.children.length">
586+
<div class="op-title">Children ({{ section.children.length }} cells, res {{ state.selectedRes + 1 }})</div>
587+
<p class="op-hint">Each child inherits the selected cell's digits and adds one new digit at resolution {{ state.selectedRes + 1 }}.</p>
564588
<table class="children-table">
565589
<thead>
566590
<tr>
567-
<th>Digit</th>
568-
<th>Formula</th>
569-
<th>{{ section.type }}</th>
570-
<th>SEQNUM (bit)</th>
571-
<th>SEQNUM (API)</th>
572-
<th>Match</th>
591+
<th>SEQNUM</th>
592+
<th>{{ section.type }} (hex)</th>
593+
<th>Digits 1–{{ state.selectedRes }}</th>
594+
<th>New digit</th>
573595
</tr>
574596
</thead>
575597
<tbody>
576-
<tr v-for="child in section.children" :key="child.digit">
577-
<td class="digit-cell-inline">{{ child.digit }}</td>
578-
<td><code>{{ section.type.toLowerCase() }} * {{ section.base }} + {{ child.digit }}</code></td>
579-
<td class="mono">{{ child[section.type.toLowerCase()]?.toString() ?? '?' }}</td>
598+
<tr class="zorder-center">
599+
<td class="mono">{{ state.selectedCell.toString() }} (selected)</td>
600+
<td class="mono">{{ section.hex }}</td>
601+
<td class="mono">{{ section.digits.join(' ') }}</td>
602+
<td>—</td>
603+
</tr>
604+
<tr v-for="child in section.children" :key="child.seqnum?.toString()">
580605
<td class="mono">{{ child.seqnum?.toString() ?? 'err' }}</td>
581-
<td class="mono">{{ section.childrenApi[child.digit]?.toString() ?? 'err' }}</td>
582-
<td>
583-
<span v-if="child.seqnum !== null && section.childrenApi[child.digit] !== undefined && child.seqnum === section.childrenApi[child.digit]" class="rt-ok">OK</span>
584-
<span v-else class="rt-fail">—</span>
585-
</td>
606+
<td class="mono">{{ child.hex }}</td>
607+
<td class="mono">{{ child.digits.slice(0, state.selectedRes).join(' ') }}</td>
608+
<td class="digit-cell-inline">{{ child.newDigit }}</td>
586609
</tr>
587610
</tbody>
588611
</table>
@@ -854,6 +877,17 @@ function loadScript(src) {
854877
background: var(--vp-c-bg, #fff);
855878
color: var(--vp-c-text-1);
856879
}
880+
.digit-quad {
881+
background: #7c3aed;
882+
color: #fff;
883+
border-color: #7c3aed;
884+
font-weight: 700;
885+
}
886+
.digit-match {
887+
background: #ecfdf5;
888+
border-color: #2ba52b;
889+
color: #2ba52b;
890+
}
857891
.digit-last {
858892
background: #e8f4fd;
859893
border-color: #2b7fd4;

0 commit comments

Comments
 (0)