nx_sketch_hll.nx source
↩ module page · 500 lines · 20185 B
1// sketch_hll.nx -- HyperLogLog cardinality sketch in NishiLang.
2//
3// Flajolet 2007 + Heule 2013 small-range correction. Sovereign-
4// tier sibling port of nishi-engine/packages/core-sketch/src/hll.ts.
5// All-i64 implementation: no f64 source-level types, no float
6// literals; harmonic-mean estimator computed in Q32.32 fixed-point
7// so the file compiles through both the C-anchor (Wheeler comparator)
8// and the NishiLang sibling.
9//
10// Per the lossless-language meta-cardinal (nishi-engine doc 20):
11// the cardinality estimate ships in an Approximate<i64> envelope
12// declaring stddev_rel = 1.04/sqrt(m) at confidence 0.6827, with
13// MaturityClass = ReferenceImpl and AdversarialSafety = Honest.
14// Bounded-loss-by-typing, not silent-loss.
15//
16// Memory: 1 byte per register. lgK=11 -> 2048 bytes; lgK=12 ->
17// 4096 bytes. Standard error: 1.04 / sqrt(2^lgK).
18//
19// Roadmap citations:
20// doc 19 -- DataSketches stomp roadmap
21// doc 20 -- universal lossless language
22// doc 21 -- research partnership not isolation
23
24// nx_safety_envelope:
25// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
26// sil_target: SIL1
27// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
28// verdict: NOT_YET_EVALUATED
29
30import "nx_syscalls.nx"
31import "nx_murmur3.nx"
32import "nx_xxhash.nx"
33import "nx_bit.nx"
34import "nx_bits.nx"
35import "nx_sketch_types.nx"
36import "nx_hll_bias_table.nx"
37import "nx_i128.nx"
38
39// lgK in [4, 10] keeps alpha_m_sq * 2^32 comfortably in signed i64
40// (max signed i64 ~9.22e18; lgK=10 puts us at ~3.23e18). lgK >= 11
41// would overflow with this Q32.32 scaling; queued for a v2 estimator
42// that uses a narrower fixed-point format (e.g. Q16.48) for the
43// upper precision range. Practical sketches today use lgK 7-10
44// (1.04/sqrt(m): 9.2% to 3.3% rel-stddev).
45const NX_HLL_LGK_MIN: i64 = 4
46const NX_HLL_LGK_MAX: i64 = 10
47
48// Murmur3 seed-mixing constants. Source: original murmur3 paper
49// (Appleby 2008) for hash-family de-correlation. Same values used by
50// DataSketches (apache/datasketches-java HllUtil.java).
51const NX_HLL_SEED_HI: i64 = 0x9747B28C
52const NX_HLL_SEED_LO: i64 = 0x36185EC0
53
54// Math constants used in the LC formula. Each named with its source.
55// ln(2) ≈ 0.693147180559945... -- math constant (lineage: log_function)
56// PPM_SCALE = 1_000_000 -- our fixed-point scale (1e6 = "parts per million")
57// We keep ln(2) at micro-precision (PPM); going finer needs a wider
58// intermediate (Q32+) which we defer to v2.
59const NX_HLL_PPM_SCALE: i64 = 1000000
60const NX_HLL_LN2_PPM: i64 = 693147 // ln(2) * 1e6
61
62// DS composite-estimator LC peg. Used to smooth the LC -> bias-corrected
63// transition: if bias_corrected_est <= (LC_PEG_NUM / LC_PEG_DEN) * K, the
64// estimator takes min(bias_corrected, linear_counting). Source:
65// DataSketches HllUtil 0.7 constant; rationale per Heule 2013 §4.
66const NX_HLL_LC_PEG_NUM: i64 = 7
67const NX_HLL_LC_PEG_DEN: i64 = 10
68
69// HLL handle. 24 bytes. regs is a separately-allocated m-byte
70// register array.
71struct Hll {
72 regs: *u8,
73 lg_k: i64,
74 m: i64,
75 seed: i64,
76}
77
78// === leading-zero helpers ==========================================
79//
80// Standard binary-search count-leading-zeros for a 32-bit value
81// (input must be in low 32 bits of an i64). Used for rho.
82
83// Delegated to nx_bits_clz32 (dispatches to bsr+xor / clzw intrinsic).
84// One machine instruction in the HLL rho path.
85// nx_clz32 is NOT redefined here. It is owned by nx_bit.nx (imported above, line 33),
86// which carries the identical `return nx_bits_clz32(x)` delegation. This file used to
87// define its own copy, so every consumer importing nx_sketch_hll.nx held TWO definitions
88// of nx_clz32 -- accepted silently by nx_cc until the duplicate-definition guard
89// (debt 1785447657) made it fail closed. Removed 2026-07-31.
90
91// === construction =================================================
92
93func nx_hll_alloc(lg_k: i64, seed: i64) -> *Hll {
94 if lg_k < NX_HLL_LGK_MIN { return 0 as *Hll }
95 if lg_k > NX_HLL_LGK_MAX { return 0 as *Hll }
96 let m: i64 = 1 << lg_k
97 let raw: *u8 = sys_mmap(24)
98 let h: *Hll = raw as *Hll
99 h.regs = sys_mmap(m)
100 h.lg_k = lg_k
101 h.m = m
102 h.seed = seed
103 var i: i64 = 0
104 while i < m {
105 h.regs[i] = 0
106 i = i + 1
107 }
108 return h
109}
110
111// === add ===========================================================
112//
113// Hash via 2x murmur3_32 with derived seeds (legacy path) -- gives a
114// 64-bit hash domain spliced from two 32-bit murmur3 outputs. Top
115// lg_k bits of hi = bucket index; suffix-then-lo drives rho.
116//
117// 2026-05-10 SELF-REMEDIATION TRACE (worked example for the
118// SELF_REMEDIATION_AS_MATHEMATICIAN cardinal):
119// Hypothesis: swap to xxh64 (single 64-bit, SMHasher-passing)
120// would tighten small-N accuracy without regressing high-N.
121// Phase 5 A/B: small-N tightened (n=100/200 became TIES vs DS)
122// BUT n=2000 regressed (mean 108 -> 142, lost BEATS).
123// Phase 6 generalization proof: REGRESSED at n=2000 cell.
124// Verdict: swap rejected. REVERTED to 2x murmur3_32.
125//
126// Root cause hypothesis (deferred until proper diagnosis):
127// xxh64 and 2x murmur3_32 both pass SMHasher. Difference at high
128// N is in higher-moment behavior that SMHasher's chi-squared
129// doesn't capture, or in alpha_m_sq_q64 calibration that has
130// subtle dependence on hash family's 4th-moment behavior. Queued
131// IMPROVEMENT_OPP: instrument register-distribution histograms
132// per hash family + per N to identify which moment diverges.
133//
134// genealogy_id: appleby_murmur3 (Austin Appleby)
135func nx_hll_add(h: *Hll, key: *u8, len: i64) -> i64 {
136 let seed_hi: i64 = h.seed ^ NX_HLL_SEED_HI
137 let seed_lo: i64 = h.seed ^ NX_HLL_SEED_LO
138 let hi: i64 = murmur3_32(seed_hi, key, len) & 0xFFFFFFFF
139 let lo: i64 = murmur3_32(seed_lo, key, len) & 0xFFFFFFFF
140
141 let idx: i64 = hi >> (32 - h.lg_k)
142
143 let upper: i64 = (hi << h.lg_k) & 0xFFFFFFFF
144 var r: i64 = 0
145 if upper != 0 {
146 r = nx_clz32(upper) + 1
147 }
148 if upper == 0 {
149 if lo != 0 {
150 r = (32 - h.lg_k) + nx_clz32(lo) + 1
151 }
152 if lo == 0 {
153 r = (64 - h.lg_k) + 1
154 }
155 }
156
157 if r > 255 { r = 255 }
158
159 let cur: i64 = h.regs[idx]
160 if cur < r {
161 h.regs[idx] = r
162 }
163 return 0
164}
165
166// === estimate (Q32.32 fixed-point harmonic mean) ===================
167//
168// E_raw = alpha_m * m^2 / sum(2^-r_j)
169// Stored as Q32.32: alpha_m_sq_q64 = alpha_m * m^2 * 2^32.
170// sum_q32 = sum_j ((1 << 32) >> r_j) (with 0 for r_j > 32)
171// estimate = alpha_m_sq_q64 / sum_q32 (i64 cardinality)
172//
173// alpha_m for lg_k >= 7 uses Flajolet's continuous approximation:
174// alpha_m = 0.7213 / (1 + 1.079 / m)
175// Lower lg_k uses tabulated values: 16->0.673, 32->0.697, 64->0.709.
176//
177// Hardcoded alpha_m_sq_q64 for lg_k in [4, 15]; computed offline
178// to avoid any floating-point operation in the hot path.
179
180// BUG FIX 2026-05-12: prior values were 877-995x too large, giving 1000x
181// over-estimates at n >> m where the classical HLL formula kicks in.
182// Smoke gates never caught this because they only exercised small-N where
183// the linear-counting correction takes over. Verified against DataSketches
184// Python 5.2.0 on n=1000 workload (was 1,137,371; should be ~1000).
185//
186// Correct formula: alpha_m * m^2 * 2^32 where alpha_m per Heule 2013:
187// m=16: alpha = 0.673
188// m=32: alpha = 0.697
189// m=64: alpha = 0.709
190// m>=128: alpha = 0.7213 / (1 + 1.079/m)
191func nx_hll_alpha_m_sq_q64(lg_k: i64) -> i64 {
192 if lg_k == 4 { return 739971325493 } // 0.673 * 16^2 * 2^32
193 if lg_k == 5 { return 3065438418239 } // 0.697 * 32^2 * 2^32
194 if lg_k == 6 { return 12472859905490 } // 0.709 * 64^2 * 2^32
195 if lg_k == 7 { return 50332686358312 } // alpha_128 * 128^2 * 2^32
196 if lg_k == 8 { return 202175761456818 } // alpha_256 * 256^2 * 2^32
197 if lg_k == 9 { return 810403740235830 } // alpha_512 * 512^2 * 2^32
198 if lg_k == 10 { return 3245027090684401 } // alpha_1024 * 1024^2 * 2^32
199 return 0
200}
201
202func nx_hll_pow2_neg_q32(r: i64) -> i64 {
203 if r >= 32 { return 0 }
204 if r <= 0 { return 1 << 32 }
205 return (1 << 32) >> r
206}
207
208// Per-lg_k thresholds for the LC-vs-classical-HLL switch.
209// Source: Heule 2013 supplementary tables (genealogy_id: heule_2013;
210// lineage_id: heule_bias_table). Each value is the maximum cardinality
211// at which the linear-counting formula is more accurate than raw HLL
212// for that lg_k. Below the threshold: use LC. Above: use raw HLL.
213const NX_HLL_HEULE_THRESH_LGK4: i64 = 10
214const NX_HLL_HEULE_THRESH_LGK5: i64 = 20
215const NX_HLL_HEULE_THRESH_LGK6: i64 = 40
216const NX_HLL_HEULE_THRESH_LGK7: i64 = 80
217const NX_HLL_HEULE_THRESH_LGK8: i64 = 220
218const NX_HLL_HEULE_THRESH_LGK9: i64 = 400
219const NX_HLL_HEULE_THRESH_LGK10: i64 = 900
220
221func nx_hll_heule_threshold(lg_k: i64) -> i64 {
222 if lg_k == 4 { return NX_HLL_HEULE_THRESH_LGK4 }
223 if lg_k == 5 { return NX_HLL_HEULE_THRESH_LGK5 }
224 if lg_k == 6 { return NX_HLL_HEULE_THRESH_LGK6 }
225 if lg_k == 7 { return NX_HLL_HEULE_THRESH_LGK7 }
226 if lg_k == 8 { return NX_HLL_HEULE_THRESH_LGK8 }
227 if lg_k == 9 { return NX_HLL_HEULE_THRESH_LGK9 }
228 if lg_k == 10 { return NX_HLL_HEULE_THRESH_LGK10 }
229 return 0
230}
231
232// log2(x) returned in PPM (parts per million), with fractional precision.
233//
234// Prior code computed integer m/zeros then floor-log2, which truncated
235// the fractional log info (e.g., log2(128/59) actually = 1.117 but
236// integer-divide gave 128/59=2 then floor-log2(2)=1 -- losing 0.117).
237//
238// This implementation keeps the fractional mantissa. For x = 2^k * (1 + frac):
239// log2(x) ≈ k + frac (linear mantissa interpolation)
240// Max error in log2 ~ 0.04 around mid-mantissa. This translates to
241// ~3% in the LC estimate -- much better than the prior ~12% from
242// dropping fractional bits entirely.
243//
244// genealogy_id: ours (linear approximation is our pragmatic choice;
245// DS uses double-precision native log2)
246// lineage_id: log_function
247func nx_hll_log2_ppm(x: i64) -> i64 {
248 if x <= 0 { return 0 }
249 if x == 1 { return 0 }
250 var k: i64 = 0
251 var t: i64 = x
252 while t > 1 { t = t >> 1; k = k + 1 }
253 let two_k: i64 = 1 << k
254 let frac_ppm: i64 = ((x - two_k) * NX_HLL_PPM_SCALE) / two_k
255 return k * NX_HLL_PPM_SCALE + frac_ppm
256}
257
258// Heule 2013 bias-correction lookup against the EMPIRICAL table
259// (hll_bias_table.nx, calibrated for OUR hash family — 2x murmur3_32 —
260// rather than the DS canonical table which was calibrated for xxhash64).
261// Table format: interleaved (raw_est_sample, corrected_n) pairs per lg_k.
262//
263// Why empirical, not DS canonical: DS's CompositeInterpolationXTable
264// expects DS's hash bias distribution. Plugging our hash family into
265// that table caused 10x worse error at small N during the 2026-05-10
266// grid-test session. Empirical regen with our actual HLL pipeline gives
267// table values matched to our hash characteristics.
268//
269// Cubic Lagrange interpolation uses nx_i128 (N2 numeric tier) for the
270// intermediate (num << 24) / den ratio to avoid i64 overflow at lg_k=10.
271// genealogy_id: heule_2013 + ours (empirical regen) + knuth_taocp_vol2 (long div for i128)
272// lineage_id: heule_bias_table
273
274func nx_hll_bias_table_get(lg_k: i64, i: i64) -> i64 {
275 if lg_k == 4 { return nx_hll_bias_lgk4(i) }
276 if lg_k == 5 { return nx_hll_bias_lgk5(i) }
277 if lg_k == 6 { return nx_hll_bias_lgk6(i) }
278 if lg_k == 7 { return nx_hll_bias_lgk7(i) }
279 if lg_k == 8 { return nx_hll_bias_lgk8(i) }
280 if lg_k == 9 { return nx_hll_bias_lgk9(i) }
281 if lg_k == 10 { return nx_hll_bias_lgk10(i) }
282 return -1
283}
284
285func nx_hll_bias_table_n(lg_k: i64) -> i64 {
286 if lg_k == 4 { return NX_HLL_BIAS_LGK4_N }
287 if lg_k == 5 { return NX_HLL_BIAS_LGK5_N }
288 if lg_k == 6 { return NX_HLL_BIAS_LGK6_N }
289 if lg_k == 7 { return NX_HLL_BIAS_LGK7_N }
290 if lg_k == 8 { return NX_HLL_BIAS_LGK8_N }
291 if lg_k == 9 { return NX_HLL_BIAS_LGK9_N }
292 if lg_k == 10 { return NX_HLL_BIAS_LGK10_N }
293 return 0
294}
295
296// Cubic Lagrange interpolation against the empirical table.
297// Uses nx_i128 (N2 numeric tier) for the (num << 24) / den intermediate
298// to avoid i64 overflow at the larger lg_k values: num can reach
299// ~1e9 in cardinality units; num << 24 = ~1.7e16 fits comfortably here
300// but i128 keeps us safe under expanded ranges.
301//
302// genealogy_id: heule_2013 + ours (empirical regen);
303// knuth_taocp_vol2 (long division) for the i128 backend
304// lineage_id: heule_bias_table
305
306const NX_HLL_LAGRANGE_Q: i64 = 24 // 24-bit fractional Q-format
307const NX_HLL_LAGRANGE_Q_SCALE: i64 = 16777216 // 1 << 24
308
309func nx_hll_bias_correct(lg_k: i64, raw_est: i64) -> i64 {
310 let n_samples: i64 = nx_hll_bias_table_n(lg_k)
311 if n_samples <= 0 { return raw_est }
312
313 // Bracket search. Table is interleaved (X, Y, X, Y, ...) -- X at
314 // even indices, Y at odd. n_samples is the count of (X, Y) pairs.
315 var i: i64 = 0
316 var found: i64 = -1
317 while i < n_samples - 1 {
318 let x_lo: i64 = nx_hll_bias_table_get(lg_k, i * 2)
319 let x_hi: i64 = nx_hll_bias_table_get(lg_k, (i + 1) * 2)
320 if x_lo <= raw_est {
321 if raw_est <= x_hi { found = i }
322 }
323 i = i + 1
324 }
325 if found < 0 { return raw_est }
326
327 // Pick 4 surrounding points; clamp to table edges.
328 var i0: i64 = found - 1
329 var i1: i64 = found
330 var i2: i64 = found + 1
331 var i3: i64 = found + 2
332 if i0 < 0 {
333 i0 = 0
334 i1 = 1
335 i2 = 2
336 i3 = 3
337 }
338 if i3 >= n_samples {
339 i3 = n_samples - 1
340 i2 = n_samples - 2
341 i1 = n_samples - 3
342 i0 = n_samples - 4
343 }
344 let x0: i64 = nx_hll_bias_table_get(lg_k, i0 * 2)
345 let y0: i64 = nx_hll_bias_table_get(lg_k, i0 * 2 + 1)
346 let x1: i64 = nx_hll_bias_table_get(lg_k, i1 * 2)
347 let y1: i64 = nx_hll_bias_table_get(lg_k, i1 * 2 + 1)
348 let x2: i64 = nx_hll_bias_table_get(lg_k, i2 * 2)
349 let y2: i64 = nx_hll_bias_table_get(lg_k, i2 * 2 + 1)
350 let x3: i64 = nx_hll_bias_table_get(lg_k, i3 * 2)
351 let y3: i64 = nx_hll_bias_table_get(lg_k, i3 * 2 + 1)
352 let dx0: i64 = raw_est - x0
353 let dx1: i64 = raw_est - x1
354 let dx2: i64 = raw_est - x2
355 let dx3: i64 = raw_est - x3
356
357 // L_k(x) = product over j!=k of (x - x_j) / (x_k - x_j)
358 // For each k compute num_k = product of three (raw - x_j) (i64);
359 // den_k = product of three (x_k - x_j) (i64); then the ratio in
360 // Q24 fixed-point via i128 intermediate (handles num << 24 overflow).
361 let num0: i64 = dx1 * dx2 * dx3
362 let den0: i64 = (x0 - x1) * (x0 - x2) * (x0 - x3)
363 let num1: i64 = dx0 * dx2 * dx3
364 let den1: i64 = (x1 - x0) * (x1 - x2) * (x1 - x3)
365 let num2: i64 = dx0 * dx1 * dx3
366 let den2: i64 = (x2 - x0) * (x2 - x1) * (x2 - x3)
367 let num3: i64 = dx0 * dx1 * dx2
368 let den3: i64 = (x3 - x0) * (x3 - x1) * (x3 - x2)
369 if den0 == 0 { return raw_est }
370 if den1 == 0 { return raw_est }
371 if den2 == 0 { return raw_est }
372 if den3 == 0 { return raw_est }
373
374 // coef_q24 = (num << 24) / den via i128 intermediate.
375 let c0_q24: i64 = nx_mulshl_div_i64(num0, 1, NX_HLL_LAGRANGE_Q, den0)
376 let c1_q24: i64 = nx_mulshl_div_i64(num1, 1, NX_HLL_LAGRANGE_Q, den1)
377 let c2_q24: i64 = nx_mulshl_div_i64(num2, 1, NX_HLL_LAGRANGE_Q, den2)
378 let c3_q24: i64 = nx_mulshl_div_i64(num3, 1, NX_HLL_LAGRANGE_Q, den3)
379
380 // term = y * coef_q24, descale by >> 24. Sum then round.
381 let sum_q24: i64 = y0 * c0_q24 + y1 * c1_q24 + y2 * c2_q24 + y3 * c3_q24
382 return sum_q24 / NX_HLL_LAGRANGE_Q_SCALE
383}
384
385// Returns (estimate, zeros_count). Caller wraps in Approximate<i64>
386// via nx_hll_query for the typed-envelope path.
387func nx_hll_estimate(h: *Hll) -> i64 {
388 let m: i64 = h.m
389 var sum_q32: i64 = 0
390 var zeros: i64 = 0
391 var i: i64 = 0
392 while i < m {
393 let r: i64 = h.regs[i]
394 if r == 0 { zeros = zeros + 1 }
395 sum_q32 = sum_q32 + nx_hll_pow2_neg_q32(r)
396 i = i + 1
397 }
398 if sum_q32 == 0 { return m } // every register saturated; degenerate
399 let alpha_m_sq_q64: i64 = nx_hll_alpha_m_sq_q64(h.lg_k)
400 if alpha_m_sq_q64 == 0 {
401 // lg_k outside hardcoded range -- fall back to crude estimate
402 return zeros
403 }
404 let raw_est: i64 = alpha_m_sq_q64 / sum_q32
405
406 // DS composite estimator (Heule 2013 + DataSketches refinement):
407 // 1. Below table range -> Linear Counting (LC) is truth
408 // 2. Above table range -> raw HLL (asymptotic bias ~ 0)
409 // 3. In-range, corrected > peg -> bias-corrected (mid+large)
410 // 4. In-range, corrected <= peg -> min(bias-corrected, LC) (small overlap)
411 //
412 // The LC peg = LC_PEG_NUM / LC_PEG_DEN * K smooths the LC/bias-corrected
413 // transition. Source: DataSketches HllUtil.HLL_NON_HIP_RSE_FACTOR
414 // (0.7 constant); rationale per Heule 2013 §4.
415 //
416 // genealogy_id: heule_2013 + datasketches (composite estimator);
417 // lineage_ids: lc_fallback (low), heule_bias_table (mid),
418 // alpha_m_correction (high)
419 let n_samples: i64 = nx_hll_bias_table_n(h.lg_k)
420 let first_x: i64 = nx_hll_bias_table_get(h.lg_k, 0)
421 let last_x: i64 = nx_hll_bias_table_get(h.lg_k, (n_samples - 1) * 2)
422 let K: i64 = 1 << h.lg_k
423 let lc_peg: i64 = (K * NX_HLL_LC_PEG_NUM) / NX_HLL_LC_PEG_DEN
424
425 // LC estimate (cheap; we already have zeros count).
426 var lc_est: i64 = -1
427 if zeros > 0 {
428 let log2_m_ppm: i64 = h.lg_k * NX_HLL_PPM_SCALE
429 let log2_z_ppm: i64 = nx_hll_log2_ppm(zeros)
430 let log2_diff_ppm: i64 = log2_m_ppm - log2_z_ppm
431 let ln_diff_ppm: i64 = (log2_diff_ppm * NX_HLL_LN2_PPM) / NX_HLL_PPM_SCALE
432 lc_est = (m * ln_diff_ppm) / NX_HLL_PPM_SCALE
433 }
434
435 if raw_est < first_x {
436 if lc_est >= 0 { return lc_est }
437 return raw_est
438 }
439 if raw_est > last_x {
440 return raw_est
441 }
442 let corrected: i64 = nx_hll_bias_correct(h.lg_k, raw_est)
443 if corrected > lc_peg {
444 return corrected
445 }
446 if lc_est >= 0 {
447 if corrected < lc_est { return corrected }
448 return lc_est
449 }
450 return corrected
451}
452
453// === typed query ===================================================
454//
455// Returns Approximate<i64> with rel_stddev envelope:
456// stddev_rel = 1.04 / sqrt(m), in parts-per-billion.
457// For lg_k=12 (m=4096), 1.04/64 = 0.01625 -> 16_250_000 ppb.
458// We hardcode per lg_k since sqrt isn't a substrate primitive yet.
459
460func nx_hll_stddev_rel_ppb(lg_k: i64) -> i64 {
461 if lg_k == 4 { return 260000000 } // 0.260
462 if lg_k == 5 { return 184000000 } // 0.184
463 if lg_k == 6 { return 130000000 } // 0.130
464 if lg_k == 7 { return 92000000 } // 0.092
465 if lg_k == 8 { return 65000000 } // 0.065
466 if lg_k == 9 { return 46000000 } // 0.046
467 if lg_k == 10 { return 32500000 } // 0.0325
468 if lg_k == 11 { return 23000000 } // 0.023
469 if lg_k == 12 { return 16250000 } // 0.01625
470 if lg_k == 13 { return 11500000 } // 0.0115
471 if lg_k == 14 { return 8125000 } // 0.008125
472 if lg_k == 15 { return 5750000 } // 0.00575
473 return 1000000000
474}
475
476func nx_hll_query(h: *Hll) -> *ApproxI64 {
477 let est: i64 = nx_hll_estimate(h)
478 let stddev_ppb: i64 = nx_hll_stddev_rel_ppb(h.lg_k)
479 return nx_approx_new(est, NX_ENV_REL_STDDEV, stddev_ppb,
480 682700000, // conf 0.6827 = ±1σ
481 NX_MATURITY_REFERENCE_IMPL,
482 NX_ADV_HONEST)
483}
484
485// === merge =========================================================
486
487func nx_hll_merge(a: *Hll, b: *Hll) -> *Hll {
488 if a.lg_k != b.lg_k { return 0 as *Hll }
489 if a.seed != b.seed { return 0 as *Hll }
490 let out: *Hll = nx_hll_alloc(a.lg_k, a.seed)
491 var i: i64 = 0
492 while i < a.m {
493 let va: i64 = a.regs[i]
494 let vb: i64 = b.regs[i]
495 if va > vb { out.regs[i] = va }
496 if va <= vb { out.regs[i] = vb }
497 i = i + 1
498 }
499 return out
500}