From e901c208c9dc4fe56cc224ee9729d74af9449fd0 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 16:55:29 +0100 Subject: [PATCH 1/6] [rntuple] implement read kReal16 --- modules/rntuple.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/rntuple.mjs b/modules/rntuple.mjs index b634f38ff..a420ed33c 100644 --- a/modules/rntuple.mjs +++ b/modules/rntuple.mjs @@ -852,10 +852,21 @@ class ReaderItem { break; case kReal16: this.func = function(obj) { - obj[this.name] = this.view.getUint16(this.o, LITTLE_ENDIAN); + const value = this.view.getUint16(this.o, LITTLE_ENDIAN); this.shift_o(2); + // reimplementing of HalfToFloat + let fbits = (value & 0x8000) << 16, + abs = value & 0x7FFF; + if (abs) { + fbits |= 0x38000000 << (abs >= 0x7C00 ? 1 : 0); + for (; abs < 0x400; abs <<= 1, fbits -= 0x800000); + fbits += abs << 13; + } + this.buf.setUint32(0, fbits, true); + obj[this.name] = this.buf.getFloat32(0, true); }; this.sz = 2; + this.buf = new DataView(new ArrayBuffer(4), 0); break; case kReal32Trunc: case kReal32Quant: From 05e0cd4db1f22623d6725532ed5f8d09030d55c0 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 16:56:00 +0100 Subject: [PATCH 2/6] [rntuple] test kReal16 reading --- demo/node/rntuple_test.cxx | 7 ++++++- demo/node/rntuple_test.js | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/demo/node/rntuple_test.cxx b/demo/node/rntuple_test.cxx index af6cbd78b..404512a94 100644 --- a/demo/node/rntuple_test.cxx +++ b/demo/node/rntuple_test.cxx @@ -40,6 +40,7 @@ void rntuple_test() // shared pointers of the given type auto IntField = model->MakeField("IntField"); auto FloatField = model->MakeField("FloatField"); + auto Float16Field = model->MakeField("Float16Field"); auto DoubleField = model->MakeField("DoubleField"); auto StringField = model->MakeField("StringField"); auto BoolField = model->MakeField("BoolField"); @@ -56,7 +57,10 @@ void rntuple_test() auto MapIntDouble = model->MakeField>("MapIntDouble"); auto MapStringBool = model->MakeField>("MapStringBool"); - + for (auto &f : model->GetMutableFieldZero()) { + if (f.GetTypeName() == "Float16Field") + f.SetColumnRepresentatives({{ROOT::ENTupleColumnType::kReal16}}); + } // We hand-over the data model to a newly created ntuple of name "F", stored in kNTupleFileName // In return, we get a unique pointer to an ntuple that we can fill @@ -66,6 +70,7 @@ void rntuple_test() *IntField = i; *FloatField = i*i; + *Float16Field = 0.1987333 * i; *DoubleField = 0.5 * i; *StringField = "entry_" + std::to_string(i); *BoolField = (i % 3 == 1); diff --git a/demo/node/rntuple_test.js b/demo/node/rntuple_test.js index 97bf619dd..e5790e078 100644 --- a/demo/node/rntuple_test.js +++ b/demo/node/rntuple_test.js @@ -78,7 +78,9 @@ else { // Setup selector to process all fields (so cluster gets loaded) const selector = new TSelector(), - fields = ['IntField', 'FloatField', 'DoubleField', 'StringField', 'BoolField', + fields = ['IntField', 'FloatField', 'DoubleField', + 'Float16Field', + 'StringField', 'BoolField', 'ArrayInt', 'VariantField', 'TupleField', 'VectString', 'VectInt', 'VectBool', 'Vect2Float', 'Vect2Bool', 'MultisetField', 'MapStringFloat', 'MapIntDouble', 'MapStringBool']; @@ -123,6 +125,7 @@ selector.Process = function(entryIndex) { IntField: entryIndex, FloatField: entryIndex * entryIndex, DoubleField: entryIndex * 0.5, + Float16Field: entryIndex * 0.1987333, StringField: `entry_${entryIndex}`, BoolField: entryIndex % 3 === 1, ArrayInt: [entryIndex + 1, entryIndex + 2, entryIndex + 3, entryIndex + 4, entryIndex + 5], From 84d6c919b4acccc9215bc3efd3c295bc05bede4d Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 19 Feb 2026 09:25:32 +0100 Subject: [PATCH 3/6] [rntuple] first working code to extract kReal32Trunc --- modules/rntuple.mjs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/modules/rntuple.mjs b/modules/rntuple.mjs index a420ed33c..e59eab48b 100644 --- a/modules/rntuple.mjs +++ b/modules/rntuple.mjs @@ -870,11 +870,32 @@ class ReaderItem { break; case kReal32Trunc: case kReal32Quant: + this.nbits = this.column.bitsOnStorage; + this.buf = new DataView(new ArrayBuffer(4), 0); this.func = function(obj) { - obj[this.name] = this.view.getUint32(this.o, LITTLE_ENDIAN); - this.shift_o(4); + let res = 0, len = this.nbits; + // extract nbits from the + while (len > 0) { + if (this.o2 === 0) { + this.byte = this.view.getUint8(this.o); + this.o2 = 8; // number of bits in the value + } + const pos = this.nbits - len; // extracted bits + if (len >= this.o2) { + res |= (this.byte & ((1 << this.o2) - 1)) << pos; // get all remaining bits + len -= this.o2; + this.o2 = 0; + this.shift_o(1); + } else { + res |= (this.byte & ((1 << len) - 1)) << pos; // get only len bits from the value + this.o2 -= len; + this.byte >>= len; + len = 0; + } + } + this.buf.setUint32(0, res << (32 - this.nbits), true); + obj[this.name] = this.buf.getFloat32(0, true); }; - this.sz = 4; break; case kInt64: case kIndex64: From fd4cbd12d478c0c8c9ffeb422b8f14b031520615 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 19 Feb 2026 09:30:07 +0100 Subject: [PATCH 4/6] Add testing of float with individual epsilon --- demo/node/rntuple_test.cxx | 21 +++++++++++++-------- demo/node/rntuple_test.js | 14 +++++++++----- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/demo/node/rntuple_test.cxx b/demo/node/rntuple_test.cxx index 404512a94..cdb3253b0 100644 --- a/demo/node/rntuple_test.cxx +++ b/demo/node/rntuple_test.cxx @@ -41,6 +41,14 @@ void rntuple_test() auto IntField = model->MakeField("IntField"); auto FloatField = model->MakeField("FloatField"); auto Float16Field = model->MakeField("Float16Field"); + model->GetMutableField("Float16Field").SetColumnRepresentatives({{ROOT::ENTupleColumnType::kReal16}}); + + auto Real32Trunc = model->MakeField("Real32Trunc"); + dynamic_cast &>(model->GetMutableField("Real32Trunc")).SetTruncated(20); + + auto Real32Quant = model->MakeField("Real32Quant"); + dynamic_cast &>(model->GetMutableField("Real32Quant")).SetQuantized(0., 1., 14); + auto DoubleField = model->MakeField("DoubleField"); auto StringField = model->MakeField("StringField"); auto BoolField = model->MakeField("BoolField"); @@ -57,11 +65,6 @@ void rntuple_test() auto MapIntDouble = model->MakeField>("MapIntDouble"); auto MapStringBool = model->MakeField>("MapStringBool"); - for (auto &f : model->GetMutableFieldZero()) { - if (f.GetTypeName() == "Float16Field") - f.SetColumnRepresentatives({{ROOT::ENTupleColumnType::kReal16}}); - } - // We hand-over the data model to a newly created ntuple of name "F", stored in kNTupleFileName // In return, we get a unique pointer to an ntuple that we can fill auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), "Data", kNTupleFileName); @@ -70,7 +73,11 @@ void rntuple_test() *IntField = i; *FloatField = i*i; - *Float16Field = 0.1987333 * i; + + *Float16Field = 0.1987333 * i; // stored as 16 bits float + *Real32Trunc = 123.45 * i; // here only 20 bits preserved + *Real32Quant = 0.03 * (i % 30); // value should be inside [0..1] + *DoubleField = 0.5 * i; *StringField = "entry_" + std::to_string(i); *BoolField = (i % 3 == 1); @@ -117,8 +124,6 @@ void rntuple_test() } Vect2Float->emplace_back(vf); Vect2Bool->emplace_back(vb); - - } writer->Fill(); diff --git a/demo/node/rntuple_test.js b/demo/node/rntuple_test.js index e5790e078..46f3e0e9d 100644 --- a/demo/node/rntuple_test.js +++ b/demo/node/rntuple_test.js @@ -79,11 +79,13 @@ else { // Setup selector to process all fields (so cluster gets loaded) const selector = new TSelector(), fields = ['IntField', 'FloatField', 'DoubleField', - 'Float16Field', + 'Float16Field', 'Real32Trunc', 'StringField', 'BoolField', 'ArrayInt', 'VariantField', 'TupleField', 'VectString', 'VectInt', 'VectBool', 'Vect2Float', 'Vect2Bool', 'MultisetField', - 'MapStringFloat', 'MapIntDouble', 'MapStringBool']; + 'MapStringFloat', 'MapIntDouble', 'MapStringBool'], + epsilonValues = { Real32Trunc: 0.5, Float16Field: 1e-2 }; + for (const f of fields) selector.addBranch(f); @@ -91,14 +93,15 @@ selector.Begin = () => { console.log('\nBegin processing to load cluster data...'); }; + // Now validate entry data const EPSILON = 1e-7; let any_error = false; -function compare(expected, value) { +function compare(expected, value, eps) { if (typeof expected === 'number') - return Math.abs(value - expected) < EPSILON; + return Math.abs(value - expected) < (eps ?? EPSILON); if (typeof expected === 'object') { if (expected.length !== undefined) { if (expected.length !== value.length) @@ -126,6 +129,7 @@ selector.Process = function(entryIndex) { FloatField: entryIndex * entryIndex, DoubleField: entryIndex * 0.5, Float16Field: entryIndex * 0.1987333, + Real32Trunc: 123.45 * entryIndex, StringField: `entry_${entryIndex}`, BoolField: entryIndex % 3 === 1, ArrayInt: [entryIndex + 1, entryIndex + 2, entryIndex + 3, entryIndex + 4, entryIndex + 5], @@ -175,7 +179,7 @@ selector.Process = function(entryIndex) { const value = this.tgtobj[field], expected = expectedValues[field]; - if (!compare(expected, value)) { + if (!compare(expected, value, epsilonValues[field])) { console.error(`FAILURE: ${field} at entry ${entryIndex} expected ${JSON.stringify(expected)}, got ${JSON.stringify(value)}`); any_error = true; } else From 12b1bef301fc894bc713036023b2323aea7a2934 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 19 Feb 2026 09:46:21 +0100 Subject: [PATCH 5/6] [rntuple] implement kReal32Quant It is simple - just rescale n-bits to min..max range --- modules/rntuple.mjs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/modules/rntuple.mjs b/modules/rntuple.mjs index e59eab48b..aa5fb54e5 100644 --- a/modules/rntuple.mjs +++ b/modules/rntuple.mjs @@ -871,7 +871,13 @@ class ReaderItem { case kReal32Trunc: case kReal32Quant: this.nbits = this.column.bitsOnStorage; - this.buf = new DataView(new ArrayBuffer(4), 0); + if (this.coltype === kReal32Trunc) + this.buf = new DataView(new ArrayBuffer(4), 0); + else { + this.factor = (this.column.maxValue - this.column.minValue) / ((1 << this.nbits) - 1); + this.min = this.column.minValue; + } + this.func = function(obj) { let res = 0, len = this.nbits; // extract nbits from the @@ -893,8 +899,11 @@ class ReaderItem { len = 0; } } - this.buf.setUint32(0, res << (32 - this.nbits), true); - obj[this.name] = this.buf.getFloat32(0, true); + if (this.buf) { + this.buf.setUint32(0, res << (32 - this.nbits), true); + obj[this.name] = this.buf.getFloat32(0, true); + } else + obj[this.name] = res * this.factor + this.min; }; break; case kInt64: @@ -1025,12 +1034,8 @@ class ReaderItem { async unzipBlob(blob, cluster_locations, page_indx) { const colEntry = cluster_locations[this.id], // Access column entry numElements = Number(colEntry.pages[page_indx].numElements), - elementSize = this.column.bitsOnStorage / 8; - - let expectedSize = numElements * elementSize; - // Special handling for boolean fields - if (this.coltype === kBit) - expectedSize = Math.ceil(numElements / 8); + elementSize = this.column.bitsOnStorage / 8, + expectedSize = Math.ceil(numElements * elementSize); // Check if data is compressed if ((colEntry.compression === 0) || (blob.byteLength === expectedSize)) From ef0995de063e01ac17d72fa026b43a4cdc9cf215 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 19 Feb 2026 09:49:01 +0100 Subject: [PATCH 6/6] [rntuple] add testing of Real32Quant Also update generated root file --- demo/node/rntuple_test.js | 5 +++-- demo/node/rntuple_test.root | Bin 3720 -> 3895 bytes 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/node/rntuple_test.js b/demo/node/rntuple_test.js index 46f3e0e9d..fbc05f35f 100644 --- a/demo/node/rntuple_test.js +++ b/demo/node/rntuple_test.js @@ -79,12 +79,12 @@ else { // Setup selector to process all fields (so cluster gets loaded) const selector = new TSelector(), fields = ['IntField', 'FloatField', 'DoubleField', - 'Float16Field', 'Real32Trunc', + 'Float16Field', 'Real32Trunc', 'Real32Quant', 'StringField', 'BoolField', 'ArrayInt', 'VariantField', 'TupleField', 'VectString', 'VectInt', 'VectBool', 'Vect2Float', 'Vect2Bool', 'MultisetField', 'MapStringFloat', 'MapIntDouble', 'MapStringBool'], - epsilonValues = { Real32Trunc: 0.5, Float16Field: 1e-2 }; + epsilonValues = { Real32Trunc: 0.3, Real32Quant: 1e-4, Float16Field: 1e-2 }; for (const f of fields) selector.addBranch(f); @@ -130,6 +130,7 @@ selector.Process = function(entryIndex) { DoubleField: entryIndex * 0.5, Float16Field: entryIndex * 0.1987333, Real32Trunc: 123.45 * entryIndex, + Real32Quant: 0.03 * (entryIndex % 30), StringField: `entry_${entryIndex}`, BoolField: entryIndex % 3 === 1, ArrayInt: [entryIndex + 1, entryIndex + 2, entryIndex + 3, entryIndex + 4, entryIndex + 5], diff --git a/demo/node/rntuple_test.root b/demo/node/rntuple_test.root index 779758184602bebafaba4f35953eca8fe693cee0..a610d96d8476167f25fbe1084da800757763a8a8 100644 GIT binary patch delta 1731 zcmZux2{e>z82*MClVL1l385@C*2FOer5Q6LqL8gDm1Rg~GL~p`%UGr?QHE~Iy&Bo2 z##P38RnkTh#l4CqQ=vt&q!^~$e{{R&bk2RxdH?4<|977Eea?5j_sP_0)HRP|Fb)DB zx&Q#60-%ToKw%E5iBQ6zbcaJI0sx%=VH(HMhC_tQWlMj6jy#T3q(0=tk)0usl+dpj z2?I)~lO)gx04tno7(<6BPqTS-0rfdICcmHj_)0S+trWP+t;DNYPSSv@0F3G_z6NvA z0!?syAdsSTVpo`FUduX&Vm!Bg7< z{Y&HhH7v9JM$#GH1Qs9(c_KV%op9t^z=h?w+FV5j(IFu&{kpo)H%b8 zFZA=#xm|AAFH^O`gqW<`H?+{Y>C!npBD_vC;pxI+e0-6U`mRyf#f+TG*;npw8TZbn zooAHT7|TYZ*P5Udx<}|oL-|O}577yNhL$v{Zke%(bSVd|uVHnXS?0PyX_xi}q22~? zWxmz@ery9!1^ldS`ci$7W|`X(P{YC1UCxtct1sLpl1HCj&!A0u-e|_z&7;MXbMJwp zJj=o!S?HAVzG}MF&?}#M3YRCvDXt!r!3aaNJxD9=66H@CmtZ)0vTSv=1}hmBI)-oq z*5xelUEep@?)z#c#r#*TnBTcH^DD)L^=h-0%trya#8Gxj5P9)s^BY=jy_9>8$iB@4 zo!nw8=hyn^cxBaTtcV`lc2jOql@WcIrE!^A@woR+G`&k|aMm?+&%WVTzF!EKHgi!r z*IukQ67|<=x3ASE6jIYRhP8H2Ert1L??x@Z&LgzM=OHCgi zK7yDSelc+-7KXmmXK?WTl;STbZFD=S=xWoc=720~M%CTy^Jrtsmqb;yG7Je3IP?XCG9Ps@b;0=S}D6Dn4UyAX>~)dD~U zzr126msiwed8wiGfkzZLz+-j-5M&i(bJsS={?o?0n>Y?Zn~ezGl_@)_NL$zd2)~e` z@rKRhXo8OxC7%r6MTvGE$)@Dq#|o%_&`_Z5aE!x>l{L;0UQKPdpq?G@(D_*WWVn)b z9S`o0VC|C(MzVh%jFLEwB&Y~45}gqWK!7*Q8V1yLN2vnxShaUgIMdVX4Yu*u=KXq}u(}k$J-w`3M4b^k$SPX6q{_czuHv z%4mA_)Zo1`u{3t+*t9hNGjpDdWDR^><-F)<%EI!4ZG;|C<6bkR#69+7MZ?uub60KY zIs7$Zw=5jY`r=h<%h0oL6MIW!JA8DfZ1Y(?jxrVxRyooLox2h5sg-t-D))SaFI4;9 zoz(o0tNvI3p2Y1eQupHzF_k`2GMnHG zWEB@jtvC7ho!K+UwS-dlj|Q~~86zR(DCjDWF8lGtpD$V-1}|X985y|F_V6O64-jo< zm+nX$eFH$Tkq%IGA4M;?mf)&i6;{w9XP_u?aGM{`Lq4_OgPfh4#LJ;j;C@H>_1_VA z|BDwTNQ6@VdHj-ltsMm_W0u9(FN^uF9~w~*BX9aYGqj->3QLnv;A6`ammd<)33WpG Wc-YSzg*m8?jx3~J5q}7G8vFx&hOb%x delta 1555 zcmZux2~d+)5Pl&ENyr}{+$0ZOK%Ch|L(t^8g^-kh7HxmeL19JcND@w|JRlgchaDe?26%3QbaV z1O&{a79cuhikA{9+Vg_6d+JfmJ7VS6UA~* zk7zmg3ZQhLEl4QgV&B&2u1nIWckz*T+mtokI@G)0IQ9|>*x>> zT|IrWfuWJHiK&@6g=%4GO{3ecu(Nk~t8$4O$F2Rn<=wuq8dTSckb?UDd@W@~-H5h9 z+aB8HP<)3Ea>U%mP@$v~4jdirGgDxIs+Hfc{$cF>##H0cb+7uW(9NaEcNfsJi;RlA z_De~t{T2u#208B^l@3UbuX!}|gnhX`V0`o4-4;kpO*QhMf)m}|ZWY{i=rMYD=%hi% z*EdbhoL97f6OSXl9(&VsGvdNJlUb)aeS*1Q;+DI0#^jX0qC&A+X#XbaUTMjhG1|4~ zrmC`$9eIIL{9kFj6ZvPdi!a~Ty&ZZ)KkGwkNYGvpky!UEt0d5%Xg{U(_ro6m`U7{`8%qU)QEhU(LB*ckJ}O z>N^+x>{!dc-;-2CyNZbU=tNG$&);TlZ zj6w0zPM3=YcL%nzl#KL`)Ev4mJ3&vfrKxIDu-%OWXBZHmt{P$0|bA4md>9fS(<7(9laQ0&J+CP!bEkjxdxp z3Lv;oy@hDkH`ZL4nJ6&!9fNzE^w z9=`g5TM?{`@@xsS{-ryfv1i8Jw<-Wt)#0qb$G`vSadtp{Np!g=r$^ECLa)izNJ(GS z>D;-mZ_d-Gvo&~borqC$YG`W{$&2@aDGJ1AIggiYoWV6c@y^=VDPeMH@#9Y$mslTf z;@^f}SPb>LChn+Ai&^TK3U)77vcvyy9(>VONq8o(dS&4_?R#+L1uAL&Y)Xc*<)lwi zS(_J5*?jKxX!WIv?{^K3f!jxCH|4bS>b&$ZAF!OLzegStH0=62-HA3z_dhjj)rQ^og_E)kESt^__z9FYlBXQD}uY*e-X{nzyP> z^?-B-Itb1IYA!lOmLuo!snREfiHNP9dLXivAB=drunh6qK!pBKNVT7s2*-^^!v5oO zHTyu|`X_gHsS<3UD%I5Dt*Pa6J|wb`tY=xsCh7jqITjT4s8)wz{?UYSm&-u98}dlW Ts4ru5xiA+^TQ@h2Sp@zC=Pe!p