diff --git a/changes.md b/changes.md index 31d666a98..4fbc1f077 100644 --- a/changes.md +++ b/changes.md @@ -3,13 +3,16 @@ ## Changes in dev 1. Implement new data types in `RNtuple` + - reduced float types kFloat16, kReal32Trunc, kReal32Quant - `std::vector` - `std::map`, `std::unordered_map`, `std::multimap`, `std::unordered_multimap` with `std::pair` - `std::set`, `std::unordered_set`, `std::multiset`, `std::unordered_multiset` - `std::array` - `std::variant` - `std::tuple` - - kFloat16, kReal32Trunc, kReal32Quant + - `std::bitset` + - `std::atomic` + - simple custom classes 1. Resort order of ranges in http request, fixing several long-standing problems #374 1. Implement for `TPie` 3d, text, title drawing including interactivity 1. Implement `TCanvas` support in `build3d` function #373 diff --git a/demo/node/rntuple_test.cxx b/demo/node/rntuple_test.cxx index cdb3253b0..cad46b363 100644 --- a/demo/node/rntuple_test.cxx +++ b/demo/node/rntuple_test.cxx @@ -13,6 +13,15 @@ #include #include #include +#include + +class TestClass { + public: + std::string fName; + std::string fTitle; + double fValue{0.}; + +}; #ifdef __ROOTCLING__ #pragma link C++ class std::map+; @@ -21,6 +30,9 @@ #pragma link C++ class std::multiset+; #pragma link C++ class std::variant+; #pragma link C++ class std::tuple+; +#pragma link C++ class std::bitset<25>+; +#pragma link C++ class std::bitset<117>+; +#pragma link C++ class TestClass+; #endif @@ -55,6 +67,10 @@ void rntuple_test() auto VariantField = model->MakeField>("VariantField"); auto TupleField = model->MakeField>("TupleField"); auto ArrayInt = model->MakeField>("ArrayInt"); + auto BitsetField = model->MakeField>("BitsetField"); + auto LargeBitsetField = model->MakeField>("LargeBitsetField"); + auto AtomicDoubleField = model->MakeField>("AtomicDoubleField"); + auto TestClassField = model->MakeField("TestClassField"); auto VectString = model->MakeField>("VectString"); auto VectInt = model->MakeField>("VectInt"); auto VectBool = model->MakeField>("VectBool"); @@ -90,6 +106,10 @@ void rntuple_test() *TupleField = { std::string("tuple_") + std::to_string(i), i * 3, (i % 3 == 1) }; + TestClassField->fName = "name_" + std::to_string(i); + TestClassField->fTitle = "title_" + std::to_string(i); + TestClassField->fValue = i; + VectString->clear(); VectInt->clear(); VectBool->clear(); @@ -103,6 +123,16 @@ void rntuple_test() Vect2Float->clear(); Vect2Bool->clear(); + BitsetField->reset(); + LargeBitsetField->reset(); + + BitsetField->set(i * 3 % 25, true); + + LargeBitsetField->set((i + 7) % 117, true); + LargeBitsetField->set((i + 35) % 117, true); + + *AtomicDoubleField = 111.444 * i; + int npx = (i + 5) % 7; for (int j = 0; j < npx; ++j) { VectString->emplace_back("str_" + std::to_string(j)); diff --git a/demo/node/rntuple_test.js b/demo/node/rntuple_test.js index fbc05f35f..5442eaeec 100644 --- a/demo/node/rntuple_test.js +++ b/demo/node/rntuple_test.js @@ -5,73 +5,79 @@ import { readHeaderFooter, rntupleProcess } from 'jsroot/rntuple'; console.log(`JSROOT version ${version}`); -let filename = './rntuple_test.root'; +let filename = './rntuple_test.root', + any_error = false; + if (process?.argv && process.argv[2]) filename = process.argv[2]; const file = await openFile(filename), - rntuple = await file.readObject('Data'); - -await readHeaderFooter(rntuple); + rntuple = await file.readObject('Data'), + builder = await readHeaderFooter(rntuple); console.log('Performing Validations and debugging info'); -if (rntuple.builder?.name !== 'Data') +if (builder?.name !== 'Data') console.error('FAILURE: name differs from expected'); else - console.log('OK: name is', rntuple.builder?.name); + console.log('OK: name is', builder?.name); -if (rntuple.builder?.description !== '') +if (builder.description !== '') console.error('FAILURE: description should be the empty string'); else console.log('OK: description is empty string'); -if (rntuple.builder?.xxhash3 === undefined || rntuple.builder.xxhash3 === null) +if (builder.xxhash3 === undefined || builder.xxhash3 === null) console.warn('WARNING: xxhash3 is missing'); else - console.log('OK: xxhash3 is', '0x' + rntuple.builder.xxhash3.toString(16).padStart(16, '0')); + console.log('OK: xxhash3 is', '0x' + builder.xxhash3.toString(16).padStart(16, '0')); // Fields Check -if (!rntuple.builder?.fieldDescriptors?.length) +if (!builder?.fieldDescriptors?.length) console.error('FAILURE: No fields deserialized'); else { - console.log(`OK: ${rntuple.builder.fieldDescriptors.length} field(s) deserialized`); - for (let i = 0; i < rntuple.builder.fieldDescriptors.length; ++i) { - const field = rntuple.builder.fieldDescriptors[i]; + console.log(`OK: ${builder.fieldDescriptors.length} field(s) deserialized`); + for (let i = 0; i < builder.fieldDescriptors.length; ++i) { + const field = builder.fieldDescriptors[i]; if (!field.fieldName || !field.typeName) console.error(`FAILURE: Field ${i} is missing name or type`); else console.log(`OK: Field ${i}: ${field.fieldName} (${field.typeName})`); if (i === 0) { - if (field.fieldName !== 'IntField' || field.typeName !== 'std::int32_t') + if (field.fieldName !== 'IntField' || field.typeName !== 'std::int32_t') { + any_error = true; console.error(`FAILURE: First field should be 'IntField (std::int32_t)' but got '${field.fieldName} (${field.typeName})'`); - } else if (i === rntuple.builder.fieldDescriptors.length - 1) { - if (field.fieldName !== '_1' || field.typeName !== 'bool') + } + } else if (i === builder.fieldDescriptors.length - 1) { + if (field.fieldName !== '_1' || field.typeName !== 'bool') { + any_error = true; console.error(`FAILURE: Last field should be '_1 (bool)' but got '${field.fieldName} (${field.typeName})'`); + } } } } // Column Check -if (!rntuple.builder?.columnDescriptors?.length) +if (!rntuple.builder?.columnDescriptors?.length) { + any_error = true; console.error('FAILURE: No columns deserialized'); -else { - console.log(`OK: ${rntuple.builder.columnDescriptors.length} column(s) deserialized`); - for (let i = 0; i < rntuple.builder.columnDescriptors.length; ++i) { - const column = rntuple.builder.columnDescriptors[i]; +} else { + console.log(`OK: ${builder.columnDescriptors.length} column(s) deserialized`); + for (let i = 0; i < builder.columnDescriptors.length; ++i) { + const column = builder.columnDescriptors[i]; if (column.fieldId === undefined || column.fieldId === null) console.error(`FAILURE: Column ${i} is missing fieldId`); else console.log(`OK: Column ${i} fieldId: ${column.fieldId} `); - if (i === 0) { - if (column.fieldId !== 0) - console.error('FAILURE: First column should be for fieldId 0'); - } else if (i === rntuple.builder.columnDescriptors.length - 1) { - if (column.fieldId !== 38) - console.error('FAILURE: Last column should be for fieldId 38'); + if ((i === 0) && (column.fieldId !== 0)) { + any_error = true; + console.error('FAILURE: First column should be for fieldId 0'); + } else if ((i === builder.columnDescriptors.length - 1) && (column.fieldId !== builder.fieldDescriptors.length - 1)) { + any_error = true; + console.error(`FAILURE: Last column should be for fieldId ${builder.fieldDescriptors.length - 1}`); } } } @@ -81,9 +87,10 @@ const selector = new TSelector(), fields = ['IntField', 'FloatField', 'DoubleField', 'Float16Field', 'Real32Trunc', 'Real32Quant', 'StringField', 'BoolField', - 'ArrayInt', 'VariantField', 'TupleField', + 'ArrayInt', 'BitsetField', 'LargeBitsetField', + 'AtomicDoubleField', 'TestClassField', 'VariantField', 'TupleField', 'VectString', 'VectInt', 'VectBool', 'Vect2Float', 'Vect2Bool', 'MultisetField', - 'MapStringFloat', 'MapIntDouble', 'MapStringBool'], + 'MapStringFloat', 'MapIntDouble', 'MapStringBool' ], epsilonValues = { Real32Trunc: 0.3, Real32Quant: 1e-4, Float16Field: 1e-2 }; for (const f of fields) @@ -97,8 +104,6 @@ selector.Begin = () => { // Now validate entry data const EPSILON = 1e-7; -let any_error = false; - function compare(expected, value, eps) { if (typeof expected === 'number') return Math.abs(value - expected) < (eps ?? EPSILON); @@ -134,6 +139,10 @@ selector.Process = function(entryIndex) { StringField: `entry_${entryIndex}`, BoolField: entryIndex % 3 === 1, ArrayInt: [entryIndex + 1, entryIndex + 2, entryIndex + 3, entryIndex + 4, entryIndex + 5], + BitsetField: 1 << entryIndex * 3 % 25, + LargeBitsetField: (1n << BigInt((entryIndex + 7) % 117)) | (1n << BigInt((entryIndex + 35) % 117)), + AtomicDoubleField: entryIndex * 111.444, + TestClassField: { _typename: 'TestClass', fName: `name_${entryIndex}`, fTitle: `title_${entryIndex}`, fValue: entryIndex }, VariantField: null, TupleField: { _0: `tuple_${entryIndex}`, _1: entryIndex*3, _2: (entryIndex % 3 === 1) }, VectString: [], @@ -183,8 +192,10 @@ selector.Process = function(entryIndex) { 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 - console.log(`OK: ${field} at entry ${entryIndex} = ${JSON.stringify(value)}`); + } else { + const res = typeof value === 'bigint' ? value.toString() + 'n' : JSON.stringify(value); + console.log(`OK: ${field} at entry ${entryIndex} = ${res}`); + } } catch (err) { console.error(`ERROR: Failed to read ${field} at entry ${entryIndex}: ${err.message}`); any_error = true; diff --git a/demo/node/rntuple_test.root b/demo/node/rntuple_test.root index a610d96d8..9c576ecff 100644 Binary files a/demo/node/rntuple_test.root and b/demo/node/rntuple_test.root differ diff --git a/modules/gui/HierarchyPainter.mjs b/modules/gui/HierarchyPainter.mjs index 49c8e88c6..a76a4f06d 100644 --- a/modules/gui/HierarchyPainter.mjs +++ b/modules/gui/HierarchyPainter.mjs @@ -558,7 +558,11 @@ function objectHierarchy(top, obj, args = undefined) { } } } - } else if ((typeof fld === 'number') || (typeof fld === 'boolean') || (typeof fld === 'bigint')) { + } else if (typeof fld === 'bigint') { + simple = true; + item._value = fld.toString() + 'n'; + item._vclass = cssValueNum; + } else if ((typeof fld === 'number') || (typeof fld === 'boolean')) { simple = true; if (key === 'fBits') item._value = '0x' + fld.toString(16); diff --git a/modules/rntuple.mjs b/modules/rntuple.mjs index bc85d8d66..4e73fa164 100644 --- a/modules/rntuple.mjs +++ b/modules/rntuple.mjs @@ -1132,6 +1132,42 @@ class ArrayReaderItem extends ReaderItem { } +/** @class reading of std::bitset + * @desc large numbers with more than 48 bits converted to BigInt + * @private */ + +class BitsetReaderItem extends ReaderItem { + + constructor(items, tgtname, size) { + super(items, tgtname); + this.size = size; + items[0].set_not_simple(); + this.bigint = size > 48; + } + + func(tgtobj) { + const tmp = {}; + let len = 0, res = this.bigint ? 0n : 0; + while (len < this.size) { + this.items[0].func(tmp); + if (tmp.bit) { + if (this.bigint) + res |= (1n << BigInt(len)); + else + res |= 1 << len; + } + len++; + } + tgtobj[this.name] = res; + } + + shift(entries) { + this.items[0].shift(entries * this.size); + } + +} + + /** @class reading std::vector and other kinds of collections * @private */ @@ -1179,7 +1215,6 @@ class VariantReaderItem extends ReaderItem { constructor(items, tgtname) { super(items, tgtname); - this.items = items; this.set_not_simple(); } @@ -1213,6 +1248,29 @@ class TupleReaderItem extends ReaderItem { } +/** @class reading custom class field + * @private */ + +class CustomClassReaderItem extends ReaderItem { + + constructor(items, tgtname, classname) { + super(items, tgtname); + this.classname = classname; + this.set_not_simple(); + } + + func(tgtobj) { + const obj = { _typename: this.classname }; + this.items.forEach(item => item.func(obj)); + tgtobj[this.name] = obj; + } + + shift(entries) { + this.items.forEach(item => item.shift(entries)); + } + +} + /** @class reading std::pair field * @private */ @@ -1354,6 +1412,10 @@ async function rntupleProcess(rntuple, selector, args = {}) { return new ArrayReaderItem([item1], tgtname, Number(field.arraySize)); } + if ((childs.length === 1) && (field.typeName.indexOf('std::atomic') === 0)) + return addFieldReading(builder, childs[0], tgtname); + + if ((childs.length > 0) && (field.typeName.indexOf('std::tuple') === 0)) { const items = []; for (let i = 0; i < childs.length; ++i) @@ -1361,6 +1423,13 @@ async function rntupleProcess(rntuple, selector, args = {}) { return new TupleReaderItem(items, tgtname); } + if ((childs.length > 0) && field.checksum && field.typeName) { + const items = []; + for (let i = 0; i < childs.length; ++i) + items.push(addFieldReading(builder, childs[i], childs[i].fieldName)); + return new CustomClassReaderItem(items, tgtname, field.typeName); + } + throw new Error(`No columns found for field '${field.fieldName}' in RNTuple`); } @@ -1370,6 +1439,12 @@ async function rntupleProcess(rntuple, selector, args = {}) { return new StringReaderItem([itemlen, itemstr], tgtname); } + if ((columns.length === 1) && (field.typeName.indexOf('std::bitset') === 0)) { + const itembit = addColumnReadout(columns[0], 'bit'); + return new BitsetReaderItem([itembit], tgtname, Number(field.arraySize)); + } + + let is_stl = false; ['vector', 'map', 'unordered_map', 'multimap', 'unordered_multimap', 'set', 'unordered_set', 'multiset', 'unordered_multiset'].forEach(name => { if (field.typeName.indexOf('std::' + name) === 0)