From ec8668d37e28dc2363b9da0e94b59c52de20c189 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 12 Jan 2024 16:41:39 +0100 Subject: [PATCH 1/7] Remove some polars code --- Cargo.lock | 1 + crates/re_query/Cargo.toml | 1 + crates/re_query/src/archetype_view.rs | 28 +++- .../re_query/tests/archetype_query_tests.rs | 122 +++++++++--------- crates/re_query/tests/type_tests.rs | 17 --- 5 files changed, 87 insertions(+), 82 deletions(-) delete mode 100644 crates/re_query/tests/type_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 3d7cf382acae..a501bb7af1fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4794,6 +4794,7 @@ dependencies = [ "re_tracing", "re_types", "re_types_core", + "smallvec", "thiserror", ] diff --git a/crates/re_query/Cargo.toml b/crates/re_query/Cargo.toml index 3a18be0e96b1..e6125aeeeb46 100644 --- a/crates/re_query/Cargo.toml +++ b/crates/re_query/Cargo.toml @@ -39,6 +39,7 @@ arrow2.workspace = true backtrace.workspace = true document-features.workspace = true itertools = { workspace = true } +smallvec.workspace = true thiserror.workspace = true # Optional dependencies: diff --git a/crates/re_query/src/archetype_view.rs b/crates/re_query/src/archetype_view.rs index 389f8f31334e..64380bcf7b92 100644 --- a/crates/re_query/src/archetype_view.rs +++ b/crates/re_query/src/archetype_view.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeMap, marker::PhantomData}; use arrow2::array::{Array, PrimitiveArray}; use re_format::arrow; -use re_log_types::{DataCell, RowId}; +use re_log_types::{DataCell, DataCellRow, RowId}; use re_types_core::{ components::InstanceKey, Archetype, Component, ComponentName, DeserializationError, DeserializationResult, Loggable, SerializationResult, @@ -519,6 +519,32 @@ impl ArchetypeView { .map(|comp| (comp.name(), comp.values.to_arrow())), )?) } + + /// Useful for tests. + pub fn to_data_cell_row_1< + 'a, + C1: re_types_core::Component + Clone + Into<::std::borrow::Cow<'a, C1>> + 'a, + >( + &self, + ) -> crate::Result { + let cell0 = DataCell::from_native(self.iter_instance_keys()); + let cell1 = DataCell::from_native_sparse(self.iter_optional_component::()?); + Ok(DataCellRow(smallvec::smallvec![cell0, cell1])) + } + + /// Useful for tests. + pub fn to_data_cell_row_2< + 'a, + C1: re_types_core::Component + Clone + Into<::std::borrow::Cow<'a, C1>> + 'a, + C2: re_types_core::Component + Clone + Into<::std::borrow::Cow<'a, C2>> + 'a, + >( + &self, + ) -> crate::Result { + let cell0 = DataCell::from_native(self.iter_instance_keys()); + let cell1 = DataCell::from_native_sparse(self.iter_optional_component::()?); + let cell2 = DataCell::from_native_sparse(self.iter_optional_component::()?); + Ok(DataCellRow(smallvec::smallvec![cell0, cell1, cell2])) + } } #[test] diff --git a/crates/re_query/tests/archetype_query_tests.rs b/crates/re_query/tests/archetype_query_tests.rs index 00d077f1e79d..469eb7f11e0f 100644 --- a/crates/re_query/tests/archetype_query_tests.rs +++ b/crates/re_query/tests/archetype_query_tests.rs @@ -1,7 +1,9 @@ mod common; +use smallvec::smallvec; + use re_data_store::DataStore; -use re_log_types::{build_frame_nr, DataRow, RowId}; +use re_log_types::{build_frame_nr, DataCell, DataCellRow, DataRow, RowId}; use re_query::query_archetype; use re_types::{ archetypes::Points2D, @@ -44,20 +46,17 @@ fn simple_query() { let arch_view = query_archetype::(&store, &timeline_query, &ent_path.into()).unwrap(); // We expect this to generate the following `DataFrame` - // ┌──────────┬───────────┬────────────┐ - // │ instance ┆ positions2D┆ colorrgba │ - // │ --- ┆ --- ┆ --- │ - // │ u64 ┆ struct[2] ┆ u32 │ - // ╞══════════╪═══════════╪════════════╡ - // │ 0 ┆ {1.0,2.0} ┆ null │ - // ├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤ - // │ 1 ┆ {3.0,4.0} ┆ 4278190080 │ - // └──────────┴───────────┴────────────┘ + // ┌──────────┬─────────────┬────────────┐ + // │ instance ┆ positions2D ┆ colorrgba │ + // │ --- ┆ --- ┆ --- │ + // │ u64 ┆ struct[2] ┆ u32 │ + // ╞══════════╪═════════════╪════════════╡ + // │ 0 ┆ {1.0,2.0} ┆ null │ + // ├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ 1 ┆ {3.0,4.0} ┆ 4278190080 │ + // └──────────┴─────────────┴────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ @@ -65,17 +64,16 @@ fn simple_query() { Some(Position2D::new(3.0, 4.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); - - //eprintln!("{df:?}"); - //eprintln!("{expected:?}"); - - common::compare_df(&expected, &arch_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - let _used = arch_view; + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); + + assert_eq!( + &arch_view.to_data_cell_row_2::().unwrap(), + &expected + ); } } @@ -118,10 +116,7 @@ fn timeless_query() { // │ 1 ┆ {3.0,4.0} ┆ 4278190080 │ // └──────────┴───────────┴────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ @@ -129,17 +124,19 @@ fn timeless_query() { Some(Position2D::new(3.0, 4.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); - common::compare_df(&expected, &arch_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - let _used = arch_view; + assert_eq!( + &arch_view.to_data_cell_row_2::().unwrap(), + &expected + ); } } @@ -180,10 +177,7 @@ fn no_instance_join_query() { // │ 1 ┆ {3.0,4.0} ┆ 16711680 │ // └──────────┴───────────┴────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ @@ -194,17 +188,19 @@ fn no_instance_join_query() { Some(Color::from_rgb(255, 0, 0)), Some(Color::from_rgb(0, 255, 0)), ]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); - common::compare_df(&expected, &arch_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - let _used = arch_view; + assert_eq!( + &arch_view.to_data_cell_row_2::().unwrap(), + &expected + ); } } @@ -240,27 +236,26 @@ fn missing_column_join_query() { // ├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤ // │ 1 ┆ {3.0,4.0} │ // └──────────┴───────────┘ - #[cfg(feature = "polars")] - { - use re_query::dataframe_util::df_builder2; + { // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ Some(Position2D::new(1.0, 2.0)), Some(Position2D::new(3.0, 4.0)), ]; - let expected = df_builder2(&instances, &positions).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); - common::compare_df(&expected, &arch_view.as_df1::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - let _used = arch_view; + assert_eq!( + &arch_view.to_data_cell_row_1::().unwrap(), + &expected + ); } } @@ -309,10 +304,7 @@ fn splatted_query() { // │ 1 ┆ {3.0,4.0} ┆ 4278190080 │ // └──────────┴───────────┴────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ @@ -323,16 +315,18 @@ fn splatted_query() { Some(Color::from_rgb(255, 0, 0)), Some(Color::from_rgb(255, 0, 0)), ]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); - common::compare_df(&expected, &arch_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - let _used = arch_view; + assert_eq!( + &arch_view.to_data_cell_row_2::().unwrap(), + &expected + ); } } diff --git a/crates/re_query/tests/type_tests.rs b/crates/re_query/tests/type_tests.rs deleted file mode 100644 index 3f4c06907d71..000000000000 --- a/crates/re_query/tests/type_tests.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[cfg(feature = "polars")] -#[test] -fn test_transform_to_polars() { - use re_types::components::Transform3D; - use re_types::datatypes::{Quaternion, TranslationRotationScale3D}; - - let transforms = vec![Some(Transform3D::from(TranslationRotationScale3D { - rotation: Some(Quaternion::from_xyzw([11.0, 12.0, 13.0, 14.0]).into()), - translation: Some([15.0, 16.0, 17.0].into()), - scale: Some([18.0, 19.0, 20.0].into()), - from_parent: true, - }))]; - - let df = re_query::dataframe_util::df_builder1(&transforms); - - assert!(df.is_ok()); -} From d0105e3c544d5476d3253c84b866575dbb5e3b17 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 12 Jan 2024 16:49:46 +0100 Subject: [PATCH 2/7] more polars replaced --- crates/re_log_types/src/data_cell.rs | 15 +++++++++- crates/re_log_types/src/example_components.rs | 1 + crates/re_query/src/archetype_view.rs | 9 ++++++ crates/re_query/src/query.rs | 28 ++++++++----------- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/re_log_types/src/data_cell.rs b/crates/re_log_types/src/data_cell.rs index f86b0e449ce1..cf0070376f85 100644 --- a/crates/re_log_types/src/data_cell.rs +++ b/crates/re_log_types/src/data_cell.rs @@ -138,7 +138,7 @@ impl PartialEq for DataCell { /// virtual calls. /// /// See #1746 for details. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct DataCellInner { /// Name of the component type used in this cell. // @@ -163,6 +163,19 @@ pub struct DataCellInner { pub(crate) values: Box, } +impl PartialEq for DataCellInner { + #[inline] + fn eq(&self, rhs: &Self) -> bool { + let Self { + name, + size_bytes: _, // we ignore the size (it may be 0 = uncomputed) + values, + } = self; + + name == &rhs.name && values.eq(&rhs.values) + } +} + // TODO(#1696): We shouldn't have to specify the component name separately, this should be // part of the metadata by using an extension. // TODO(#1696): Check that the array is indeed a leaf / component type when building a cell from an diff --git a/crates/re_log_types/src/example_components.rs b/crates/re_log_types/src/example_components.rs index bb958ff4e389..1cb8e906ef9c 100644 --- a/crates/re_log_types/src/example_components.rs +++ b/crates/re_log_types/src/example_components.rs @@ -4,6 +4,7 @@ use re_types_core::{Loggable, SizeBytes}; // ---------------------------------------------------------------------------- +#[derive(Debug)] pub struct MyPoints; impl re_types_core::Archetype for MyPoints { diff --git a/crates/re_query/src/archetype_view.rs b/crates/re_query/src/archetype_view.rs index 64380bcf7b92..06673bdb8cc3 100644 --- a/crates/re_query/src/archetype_view.rs +++ b/crates/re_query/src/archetype_view.rs @@ -116,6 +116,15 @@ impl ComponentWithInstances { values: DataCell::from_arrow(C::name(), values), }) } + + #[inline] + pub fn into_data_cell_row(self) -> DataCellRow { + let Self { + instance_keys, + values, + } = self; + DataCellRow(smallvec::smallvec![instance_keys, values]) + } } /// Iterator over a single [`Component`] joined onto a primary [`Component`] diff --git a/crates/re_query/src/query.rs b/crates/re_query/src/query.rs index 8794c80f6c44..7cfdf9ed4077 100644 --- a/crates/re_query/src/query.rs +++ b/crates/re_query/src/query.rs @@ -209,9 +209,11 @@ pub fn __populate_example_store() -> DataStore { #[cfg(test)] #[cfg(feature = "testing")] fn simple_get_component() { + use smallvec::smallvec; + use re_data_store::LatestAtQuery; - use re_log_types::example_components::MyPoint; - use re_log_types::Timeline; + use re_log_types::{example_components::MyPoint, DataCellRow}; + use re_log_types::{DataCell, Timeline}; let store = __populate_example_store(); @@ -221,21 +223,19 @@ fn simple_get_component() { let (_, component) = get_component_with_instances(&store, &query, &ent_path.into(), MyPoint::name()).unwrap(); - #[cfg(feature = "polars")] { - let df = component.as_df::().unwrap(); - eprintln!("{df:?}"); + let row = component.into_data_cell_row(); + eprintln!("{row:?}"); let instances = vec![Some(InstanceKey(42)), Some(InstanceKey(96))]; let positions = vec![Some(MyPoint::new(1.0, 2.0)), Some(MyPoint::new(3.0, 4.0))]; - let expected = crate::dataframe_util::df_builder2(&instances, &positions).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + ]); - assert_eq!(expected, df); - } - #[cfg(not(feature = "polars"))] - { - let _used = component; + assert_eq!(row, expected); } } @@ -271,9 +271,5 @@ fn simple_query_archetype() { assert_eq!(expected_positions, view_positions.as_slice()); assert_eq!(expected_colors, view_colors.as_slice()); - #[cfg(feature = "polars")] - { - let df = arch_view.as_df2::().unwrap(); - eprintln!("{df:?}"); - } + eprintln!("{arch_view:?}"); } From cf9b77eaaf221bd737d6620710f9bbf47b15ef8b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 12 Jan 2024 20:00:45 +0100 Subject: [PATCH 3/7] Kill it all --- .../re_query/tests/archetype_query_tests.rs | 2 - .../re_query/tests/archetype_range_tests.rs | 300 +++++++++++------- crates/re_query/tests/common/mod.rs | 12 - 3 files changed, 184 insertions(+), 130 deletions(-) delete mode 100644 crates/re_query/tests/common/mod.rs diff --git a/crates/re_query/tests/archetype_query_tests.rs b/crates/re_query/tests/archetype_query_tests.rs index 469eb7f11e0f..cd2c0817eebf 100644 --- a/crates/re_query/tests/archetype_query_tests.rs +++ b/crates/re_query/tests/archetype_query_tests.rs @@ -1,5 +1,3 @@ -mod common; - use smallvec::smallvec; use re_data_store::DataStore; diff --git a/crates/re_query/tests/archetype_range_tests.rs b/crates/re_query/tests/archetype_range_tests.rs index a269ed6a61fe..0207f88811bb 100644 --- a/crates/re_query/tests/archetype_range_tests.rs +++ b/crates/re_query/tests/archetype_range_tests.rs @@ -1,7 +1,7 @@ -mod common; +use smallvec::smallvec; use re_data_store::{DataStore, TimeInt, TimeRange}; -use re_log_types::{build_frame_nr, DataRow, EntityPath, RowId}; +use re_log_types::{build_frame_nr, DataCell, DataCellRow, DataRow, EntityPath, RowId}; use re_query::range_archetype; use re_types::{ archetypes::Points2D, @@ -102,13 +102,10 @@ fn simple_range() { // │ 1 ┆ {30.0,40.0} ┆ null │ // └─────────────┴──────────────┴─────────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Frame #123 - let (time, ent_view) = &results[0]; + let (time, arch_view) = &results[0]; let time = time.unwrap(); // Build expected df manually @@ -118,17 +115,25 @@ fn simple_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { // Frame #323 - let (time, ent_view) = &results[1]; + let (time, arch_view) = &results[1]; let time = time.unwrap(); // Build expected df manually @@ -138,18 +143,20 @@ fn simple_range() { Some(Position2D::new(30.0, 40.0)), ]; let colors = vec![Some(Color::from_rgb(255, 0, 0)), None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - _ = results; + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); } // --- Second test: `[timepoint1, timepoint3]` --- @@ -186,13 +193,10 @@ fn simple_range() { // │ 1 ┆ {30.0,40.0} ┆ null │ // └────────────────────┴───────────────┴─────────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Frame #123 - let (time, ent_view) = &results[0]; + let (time, arch_view) = &results[0]; let time = time.unwrap(); // Build expected df manually @@ -202,17 +206,25 @@ fn simple_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors: Vec> = vec![None, None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { // Frame #323 - let (time, ent_view) = &results[1]; + let (time, arch_view) = &results[1]; let time = time.unwrap(); // Build expected df manually @@ -222,18 +234,20 @@ fn simple_range() { Some(Position2D::new(30.0, 40.0)), ]; let colors = vec![Some(Color::from_rgb(255, 0, 0)), None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - _ = results; + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); } } @@ -383,13 +397,10 @@ fn timeless_range() { // │ 1 ┆ {30.0,40.0} ┆ null │ // └────────────────────┴───────────────┴─────────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Frame #123 - let (time, ent_view) = &results[0]; + let (time, arch_view) = &results[0]; let time = time.unwrap(); // Build expected df manually @@ -399,17 +410,25 @@ fn timeless_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { // Frame #323 - let (time, ent_view) = &results[1]; + let (time, arch_view) = &results[1]; let time = time.unwrap(); // Build expected df manually @@ -419,18 +438,20 @@ fn timeless_range() { Some(Position2D::new(30.0, 40.0)), ]; let colors = vec![Some(Color::from_rgb(255, 0, 0)), None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - _ = results; + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); } // --- Second test: `[timepoint1, timepoint3]` --- @@ -476,13 +497,10 @@ fn timeless_range() { // │ 1 ┆ {30.0,40.0} ┆ null │ // └────────────────────┴───────────────┴─────────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Frame #122 (all timeless) - let (time, ent_view) = &results[0]; + let (time, arch_view) = &results[0]; let time = time.unwrap(); // Build expected df manually @@ -492,17 +510,24 @@ fn timeless_range() { Some(Position2D::new(30.0, 40.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(122), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); // Frame #123 (partially timeless) - let (time, ent_view) = &results[1]; + let (time, arch_view) = &results[1]; let time = time.unwrap(); // Build expected df manually @@ -512,17 +537,25 @@ fn timeless_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { // Frame #323 - let (time, ent_view) = &results[2]; + let (time, arch_view) = &results[2]; let time = time.unwrap(); // Build expected df manually @@ -532,18 +565,20 @@ fn timeless_range() { Some(Position2D::new(30.0, 40.0)), ]; let colors = vec![Some(Color::from_rgb(255, 0, 0)), None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - _ = results; + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); } // --- Third test: `[-inf, +inf]` --- @@ -594,13 +629,10 @@ fn timeless_range() { // │ 1 ┆ {30.0,40.0} ┆ null │ // └────────────────────┴───────────────┴─────────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Timeless #1 - let (time, ent_view) = &results[0]; + let (time, arch_view) = &results[0]; // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; @@ -609,17 +641,24 @@ fn timeless_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors: Vec> = vec![None, None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(&None, time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); // Timeless #2 - let (time, ent_view) = &results[1]; + let (time, arch_view) = &results[1]; // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; @@ -628,17 +667,24 @@ fn timeless_range() { Some(Position2D::new(30.0, 40.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(&None, time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); // Frame #123 (partially timeless) - let (time, ent_view) = &results[2]; + let (time, arch_view) = &results[2]; let time = time.unwrap(); // Build expected df manually @@ -648,17 +694,25 @@ fn timeless_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { // Frame #323 - let (time, ent_view) = &results[3]; + let (time, arch_view) = &results[3]; let time = time.unwrap(); // Build expected df manually @@ -668,18 +722,20 @@ fn timeless_range() { Some(Position2D::new(30.0, 40.0)), ]; let colors = vec![Some(Color::from_rgb(255, 0, 0)), None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - _ = results; + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); } } @@ -778,13 +834,10 @@ fn simple_splatted_range() { assert_eq!(results.len(), 2); - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Frame #123 - let (time, ent_view) = &results[0]; + let (time, arch_view) = &results[0]; let time = time.unwrap(); // Build expected df manually @@ -794,17 +847,26 @@ fn simple_splatted_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { // Frame #323 - let (time, ent_view) = &results[1]; + let (time, arch_view) = &results[1]; let time = time.unwrap(); // Build expected df manually @@ -818,19 +880,18 @@ fn simple_splatted_range() { Some(Color::from_rgb(0, 255, 0)), ]; - let df = ent_view.as_df2::().unwrap(); - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let df = arch_view.to_data_cell_row_2::().unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); - common::compare_df(&expected, &df); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - _ = results; + assert_eq!(&expected, &df); } // --- Second test: `[timepoint1, timepoint3]` --- @@ -867,13 +928,10 @@ fn simple_splatted_range() { // │ 1 ┆ {30.0,40.0} ┆ 16711680 │ // └────────────────────┴───────────────┴─────────────────┘ - #[cfg(feature = "polars")] { - use re_query::dataframe_util::df_builder3; - // Frame #123 - let (time, ent_view) = &results[0]; + let (time, arch_view) = &results[0]; let time = time.unwrap(); // Build expected df manually @@ -883,17 +941,25 @@ fn simple_splatted_range() { Some(Position2D::new(3.0, 4.0)), ]; let colors: Vec> = vec![None, None]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { // Frame #323 - let (time, ent_view) = &results[1]; + let (time, arch_view) = &results[1]; let time = time.unwrap(); // Build expected df manually @@ -906,17 +972,19 @@ fn simple_splatted_range() { Some(Color::from_rgb(0, 255, 0)), Some(Color::from_rgb(0, 255, 0)), ]; - let expected = df_builder3(&instances, &positions, &colors).unwrap(); + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); - common::compare_df(&expected, &ent_view.as_df2::().unwrap()); - } - #[cfg(not(feature = "polars"))] - { - //TODO(jleibs): non-polars test validation - _ = results; + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); } } diff --git a/crates/re_query/tests/common/mod.rs b/crates/re_query/tests/common/mod.rs deleted file mode 100644 index e97134c8c66a..000000000000 --- a/crates/re_query/tests/common/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(feature = "polars")] -use polars_core::prelude::*; - -#[cfg(feature = "polars")] -pub fn compare_df(df1: &DataFrame, df2: &DataFrame) { - let mut cols1 = df1.get_column_names(); - cols1.sort(); - let mut cols2 = df2.get_column_names(); - cols2.sort(); - - assert_eq!(df1.select(cols1).unwrap(), df2.select(cols2).unwrap()); -} From 531a92d2c2bda68b5ede4a84253f3ad137c3f3f3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 12 Jan 2024 20:13:40 +0100 Subject: [PATCH 4/7] check readability with to_data_table --- crates/re_data_store/tests/correctness.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/re_data_store/tests/correctness.rs b/crates/re_data_store/tests/correctness.rs index 759c85b8eb71..ba1c40df94af 100644 --- a/crates/re_data_store/tests/correctness.rs +++ b/crates/re_data_store/tests/correctness.rs @@ -552,11 +552,8 @@ fn gc_correct() { check_still_readable(&store); } -fn check_still_readable(_store: &DataStore) { - #[cfg(feature = "polars")] - { - _ = _store.to_dataframe(); // simple way of checking that everything is still readable - } +fn check_still_readable(store: &DataStore) { + store.to_data_table().unwrap(); // simple way of checking that everything is still readable } // This used to panic because the GC will decrement the metadata_registry size trackers before From df6f82d7b1b8950d590e8b65006fa95fef7e07b9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 12 Jan 2024 21:04:13 +0100 Subject: [PATCH 5/7] Add DataStore::to_rows --- crates/re_data_store/src/store_dump.rs | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/re_data_store/src/store_dump.rs b/crates/re_data_store/src/store_dump.rs index 97a06ed8adb7..e83a72b9e65f 100644 --- a/crates/re_data_store/src/store_dump.rs +++ b/crates/re_data_store/src/store_dump.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use arrow2::Either; use re_log_types::{ - DataCellColumn, DataTable, ErasedTimeVec, RowIdVec, TableId, TimeRange, Timeline, + DataCellColumn, DataRow, DataTable, ErasedTimeVec, RowIdVec, TableId, TimeRange, Timeline, }; use crate::{ @@ -13,13 +13,14 @@ use crate::{ // --- impl DataStore { - /// Serializes the entire datastore into one big sorted [`DataTable`]. + /// Serializes the entire datastore into one big sorted list of [`DataRow`]. /// /// Individual [`re_log_types::DataRow`]s that were split apart due to bucketing are merged back together. /// /// Beware: this is extremely costly, don't use this in hot paths. - pub fn to_data_table(&self) -> re_log_types::DataReadResult { - use re_log_types::{DataRow, RowId}; + pub fn to_rows(&self) -> re_log_types::DataReadResult> { + use re_log_types::RowId; + re_tracing::profile_function!(); let mut rows = ahash::HashMap::::default(); for table in self.to_data_tables(None) { @@ -39,7 +40,23 @@ impl DataStore { } let mut rows = rows.into_values().collect::>(); - rows.sort_by_key(|row| (row.timepoint.clone(), row.row_id)); + { + re_tracing::profile_scope!("sort_rows"); + rows.sort_by_key(|row| (row.timepoint.clone(), row.row_id)); + } + + Ok(rows) + } + + /// Serializes the entire datastore into one big sorted [`DataTable`]. + /// + /// Individual [`re_log_types::DataRow`]s that were split apart due to bucketing are merged back together. + /// + /// Beware: this is extremely costly, don't use this in hot paths. + pub fn to_data_table(&self) -> re_log_types::DataReadResult { + re_tracing::profile_function!(); + + let rows = self.to_rows()?; Ok(re_log_types::DataTable::from_rows( re_log_types::TableId::new(), From 99f09dea692faf3152d75d68dc84b55b46eb3869 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 12 Jan 2024 20:45:55 +0100 Subject: [PATCH 6/7] Replace polars again --- crates/re_data_store/tests/dump.rs | 161 +++++++++++++++++++---------- 1 file changed, 109 insertions(+), 52 deletions(-) diff --git a/crates/re_data_store/tests/dump.rs b/crates/re_data_store/tests/dump.rs index e4c68591c359..129a4f26914b 100644 --- a/crates/re_data_store/tests/dump.rs +++ b/crates/re_data_store/tests/dump.rs @@ -8,7 +8,9 @@ use re_data_store::{ test_row, test_util::sanity_unwrap, DataStore, DataStoreStats, GarbageCollectionOptions, TimeInt, TimeRange, Timeline, }; -use re_log_types::{build_frame_nr, build_log_time, DataTable, EntityPath, TableId}; +use re_log_types::{ + build_frame_nr, build_log_time, DataRow, DataTable, EntityPath, RowId, TableId, +}; use re_types::components::InstanceKey; use re_types::datagen::{build_some_colors, build_some_instances, build_some_positions2d}; use re_types_core::Loggable as _; @@ -31,6 +33,55 @@ fn insert_table_with_retries(store: &mut DataStore, table: &DataTable) { } } +// Panic on RowId clash. +fn insert_table(store: &mut DataStore, table: &DataTable) { + for row in table.to_rows() { + let row = row.unwrap(); + store.insert_row(&row).unwrap(); + } +} + +// --- + +/// Allows adding more data to the same `RowId`. +#[derive(Default)] +struct RowSet(ahash::HashMap); + +impl RowSet { + fn insert_tables(&mut self, tables: impl Iterator) { + for table in tables { + self.insert_table(&table); + } + } + + fn insert_table(&mut self, table: &DataTable) { + for row in table.to_rows() { + self.insert_row(row.unwrap()); + } + } + + fn insert_row(&mut self, row: re_log_types::DataRow) { + match self.0.entry(row.row_id()) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + for (timeline, time) in row.timepoint() { + entry.get_mut().timepoint.insert(*timeline, *time); + } + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(row); + } + } + } + + fn insert_into(self, store: &mut DataStore) { + let mut rows = self.0.into_values().collect::>(); + rows.sort_by_key(|row| (row.timepoint.clone(), row.row_id)); + for row in rows { + store.insert_row(&row).unwrap(); + } + } +} + // --- Dump --- #[test] @@ -69,17 +120,6 @@ fn data_store_dump() { } fn data_store_dump_impl(store1: &mut DataStore, store2: &mut DataStore, store3: &mut DataStore) { - // helper to insert a table both as a temporal and timeless payload - let insert_table = |store: &mut DataStore, table: &DataTable| { - // insert temporal - insert_table_with_retries(store, table); - - // insert timeless - let mut table_timeless = table.clone(); - table_timeless.col_timelines = Default::default(); - insert_table_with_retries(store, &table_timeless); - }; - let ent_paths = ["this/that", "other", "yet/another/one"]; let tables = ent_paths .iter() @@ -88,34 +128,45 @@ fn data_store_dump_impl(store1: &mut DataStore, store2: &mut DataStore, store3: // Fill the first store. for table in &tables { + // insert temporal insert_table(store1, table); + + // insert timeless + let mut table_timeless = table.clone(); + table_timeless.col_timelines = Default::default(); + insert_table_with_retries(store1, &table_timeless); } sanity_unwrap(store1); // Dump the first store into the second one. - for table in store1.to_data_tables(None) { - insert_table_with_retries(store2, &table); + { + // We use a RowSet instead of a DataTable to handle duplicate RowIds. + let mut row_set = RowSet::default(); + row_set.insert_tables(store1.to_data_tables(None)); + row_set.insert_into(store2); + sanity_unwrap(store2); } - sanity_unwrap(store2); // Dump the second store into the third one. - for table in store2.to_data_tables(None) { - insert_table_with_retries(store3, &table); + { + let mut row_set = RowSet::default(); + row_set.insert_tables(store2.to_data_tables(None)); + row_set.insert_into(store3); + sanity_unwrap(store3); } - sanity_unwrap(store3); - #[cfg(feature = "polars")] { - let store1_df = store1.to_dataframe(); - let store2_df = store2.to_dataframe(); - let store3_df = store3.to_dataframe(); + let table_id = TableId::new(); // Reuse TableId so == works + let table1 = DataTable::from_rows(table_id, store1.to_rows().unwrap()); + let table2 = DataTable::from_rows(table_id, store2.to_rows().unwrap()); + let table3 = DataTable::from_rows(table_id, store3.to_rows().unwrap()); assert!( - store1_df == store2_df, - "First & second stores differ:\n{store1_df}\n{store2_df}" + table1 == table2, + "First & second stores differ:\n{table1}\n{table2}" ); assert!( - store1_df == store3_df, - "First & third stores differ:\n{store1_df}\n{store3_df}" + table1 == table3, + "First & third stores differ:\n{table1}\n{table3}" ); } @@ -183,39 +234,45 @@ fn data_store_dump_filtered_impl(store1: &mut DataStore, store2: &mut DataStore) // Fill the first store. for table in &tables { - insert_table_with_retries(store1, table); + insert_table(store1, table); } sanity_unwrap(store1); - // Dump frame1 from the first store into the second one. - for table in store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame1, frame1)).into()) { - insert_table_with_retries(store2, &table); - } - // Dump frame2 from the first store into the second one. - for table in store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame2, frame2)).into()) { - insert_table_with_retries(store2, &table); - } - // Dump frame3 from the first store into the second one. - for table in store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame3, frame3)).into()) { - insert_table_with_retries(store2, &table); - } - // Dump the other frame3 from the first store into the second one. - for table in store1.to_data_tables((timeline_log_time, TimeRange::new(frame3, frame3)).into()) { - insert_table_with_retries(store2, &table); - } - // Dump frame4 from the first store into the second one. - for table in store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame4, frame4)).into()) { - insert_table_with_retries(store2, &table); - } + // We use a RowSet instead of a DataTable to handle duplicate RowIds. + let mut row_set = RowSet::default(); + + // Dump frame1 from the first store. + row_set.insert_tables( + store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame1, frame1)).into()), + ); + // Dump frame2 from the first store. + row_set.insert_tables( + store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame2, frame2)).into()), + ); + // Dump frame3 from the first store. + row_set.insert_tables( + store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame3, frame3)).into()), + ); + // Dump frame3 _from the other timeline_, from the first store. + // This will produce the same RowIds again! + row_set.insert_tables( + store1.to_data_tables((timeline_log_time, TimeRange::new(frame3, frame3)).into()), + ); + // Dump frame4 from the first store. + row_set.insert_tables( + store1.to_data_tables((timeline_frame_nr, TimeRange::new(frame4, frame4)).into()), + ); + + row_set.insert_into(store2); sanity_unwrap(store2); - #[cfg(feature = "polars")] { - let store1_df = store1.to_dataframe(); - let store2_df = store2.to_dataframe(); + let table_id = TableId::new(); // Reuse TableId so == works + let table1 = DataTable::from_rows(table_id, store1.to_rows().unwrap()); + let table2 = DataTable::from_rows(table_id, store2.to_rows().unwrap()); assert!( - store1_df == store2_df, - "First & second stores differ:\n{store1_df}\n{store2_df}" + table1 == table2, + "First & second stores differ:\n{table1}\n{table2}" ); } From 5f0bfe1996e12b974d6ed306977628e8e9572386 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 15 Jan 2024 10:01:43 +0100 Subject: [PATCH 7/7] add more asserts Co-authored-by: Clement Rey --- crates/re_data_store/tests/dump.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/re_data_store/tests/dump.rs b/crates/re_data_store/tests/dump.rs index 129a4f26914b..3bc6f972e03f 100644 --- a/crates/re_data_store/tests/dump.rs +++ b/crates/re_data_store/tests/dump.rs @@ -63,6 +63,9 @@ impl RowSet { fn insert_row(&mut self, row: re_log_types::DataRow) { match self.0.entry(row.row_id()) { std::collections::hash_map::Entry::Occupied(mut entry) => { + assert_eq!(entry.get().entity_path(), row.entity_path()); + assert_eq!(entry.get().cells(), row.cells()); + assert_eq!(entry.get().num_instances(), row.num_instances()); for (timeline, time) in row.timepoint() { entry.get_mut().timepoint.insert(*timeline, *time); }