diff --git a/Cargo.toml b/Cargo.toml index 0358f38d9..7497abbd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ thiserror = "1.0.64" # core downcast-rs = "1.2.1" +loom = "0.7.2" num-traits = "0.2.19" stm = "0.4.0" vtkio = { version = "0.7.0-rc1", default-features = false } diff --git a/honeycomb-core/Cargo.toml b/honeycomb-core/Cargo.toml index 92b632a72..c005a4879 100644 --- a/honeycomb-core/Cargo.toml +++ b/honeycomb-core/Cargo.toml @@ -20,6 +20,7 @@ utils = [] [dependencies] downcast-rs.workspace = true +loom.workspace= true num-traits.workspace = true stm.workspace = true thiserror.workspace = true diff --git a/honeycomb-core/src/attributes/mod.rs b/honeycomb-core/src/attributes/mod.rs index 86ef86c06..d7e33709e 100644 --- a/honeycomb-core/src/attributes/mod.rs +++ b/honeycomb-core/src/attributes/mod.rs @@ -14,5 +14,6 @@ pub use traits::{AttributeBind, AttributeStorage, AttributeUpdate, UnknownAttrib // ------ TESTS +#[allow(clippy::float_cmp)] #[cfg(test)] mod tests; diff --git a/honeycomb-core/src/attributes/tests.rs b/honeycomb-core/src/attributes/tests.rs index f7e4ac9c9..e53ee609a 100644 --- a/honeycomb-core/src/attributes/tests.rs +++ b/honeycomb-core/src/attributes/tests.rs @@ -1,16 +1,24 @@ // ------ IMPORTS +use loom::sync::Arc; +use stm::{atomically, StmError, Transaction, TransactionControl}; + use super::{ - /*AttrCompactVec,*/ AttrSparseVec, AttrStorageManager, AttributeBind, AttributeStorage, - AttributeUpdate, UnknownAttributeStorage, + AttrSparseVec, AttrStorageManager, AttributeBind, AttributeStorage, AttributeUpdate, + UnknownAttributeStorage, +}; +use crate::{ + cmap::EdgeIdentifier, + prelude::{CMap2, CMapBuilder, FaceIdentifier, OrbitPolicy, Vertex2, VertexIdentifier}, }; -use crate::prelude::{CMap2, CMapBuilder, FaceIdentifier, OrbitPolicy, Vertex2, VertexIdentifier}; use std::any::Any; // ------ CONTENT // --- basic structure implementation +// vertex bound + #[derive(Clone, Copy, Debug, Default, PartialEq)] struct Temperature { pub val: f32, @@ -48,6 +56,76 @@ impl From for Temperature { } } +#[derive(Debug, Clone, Copy, Default, PartialEq)] +struct Weight(pub u32); + +impl AttributeUpdate for Weight { + fn merge(attr1: Self, attr2: Self) -> Self { + Self(attr1.0 + attr2.0) + } + + fn split(attr: Self) -> (Self, Self) { + // adding the % to keep things conservative + (Weight(attr.0 / 2 + attr.0 % 2), Weight(attr.0 / 2)) + } +} + +impl AttributeBind for Weight { + type StorageType = AttrSparseVec; + type IdentifierType = VertexIdentifier; + const BIND_POLICY: OrbitPolicy = OrbitPolicy::Vertex; +} + +// edge bound + +#[derive(Debug, Clone, PartialEq, Copy)] +struct Length(pub f32); + +impl AttributeUpdate for Length { + fn merge(attr1: Self, attr2: Self) -> Self { + Length((attr1.0 + attr2.0) / 2.0) + } + + fn split(attr: Self) -> (Self, Self) { + (attr, attr) + } +} + +impl AttributeBind for Length { + type IdentifierType = EdgeIdentifier; + type StorageType = AttrSparseVec; + const BIND_POLICY: OrbitPolicy = OrbitPolicy::Edge; +} + +// face bound + +fn mean(a: u8, b: u8) -> u8 { + ((u16::from(a) + u16::from(b)) / 2) as u8 +} + +#[derive(Debug, Clone, Copy, Default, PartialEq)] +struct Color(pub u8, pub u8, pub u8); + +impl AttributeUpdate for Color { + fn merge(attr1: Self, attr2: Self) -> Self { + Self( + mean(attr1.0, attr2.0), + mean(attr1.1, attr2.1), + mean(attr1.2, attr2.2), + ) + } + + fn split(attr: Self) -> (Self, Self) { + (attr, attr) + } +} + +impl AttributeBind for Color { + type StorageType = AttrSparseVec; + type IdentifierType = FaceIdentifier; + const BIND_POLICY: OrbitPolicy = OrbitPolicy::Face; +} + // --- usual workflow test #[test] @@ -102,9 +180,295 @@ fn temperature_map() { assert_eq!(map.vertex(map.vertex_id(2)), Some(Vertex2::from((1., 0.)))); assert_eq!(map.vertex(map.vertex_id(3)), Some(Vertex2::from((1., 0.)))); } +#[test] +fn test_attribute_operations() { + let mut manager = AttrStorageManager::default(); + manager.add_storage::(5); + + // Test set and get + manager.set_attribute(0, Temperature::from(25.0)); + assert_eq!( + manager.get_attribute::(0), + Some(Temperature::from(25.0)) + ); + + // Test insert + manager.insert_attribute(1, Temperature::from(30.0)); + assert_eq!( + manager.get_attribute::(1), + Some(Temperature::from(30.0)) + ); + + // Test replace + let old_val = manager.replace_attribute(0, Temperature::from(27.0)); + assert_eq!(old_val, Some(Temperature::from(25.0))); + assert_eq!( + manager.get_attribute::(0), + Some(Temperature::from(27.0)) + ); + + // Test remove + let removed = manager.remove_attribute::(0); + assert_eq!(removed, Some(Temperature::from(27.0))); + assert_eq!(manager.get_attribute::(0), None); +} + +#[test] +fn test_merge_attributes() { + let mut manager = AttrStorageManager::default(); + manager.add_storage::(5); + + // Setup initial values + manager.set_attribute(0, Temperature::from(20.0)); + manager.set_attribute(1, Temperature::from(30.0)); + + // Test merge + manager.merge_attribute::(2, 0, 1); + + // The exact result depends on how merge is implemented in AttributeStorage + // Just verify that something was stored at the output location + assert!(manager.get_attribute::(2).is_some()); +} + +#[test] +fn test_split_attributes() { + let mut manager = AttrStorageManager::default(); + manager.add_storage::(5); + + // Setup initial value + manager.set_attribute(0, Temperature::from(25.0)); + + // Test split + manager.split_attribute::(1, 2, 0); + + // The exact results depend on how split is implemented in AttributeStorage + // Just verify that something was stored at both output locations + assert!(manager.get_attribute::(1).is_some()); + assert!(manager.get_attribute::(2).is_some()); +} + +#[test] +fn test_extend_all_storages() { + let mut manager = AttrStorageManager::default(); + + // Add storages of different types + manager.add_storage::(2); + manager.add_storage::(2); + + // Extend all storages + manager.extend_storages(3); + + // Check that all storages were extended + let temp_storage = manager.get_storage::().unwrap(); + let length_storage = manager.get_storage::().unwrap(); + + assert_eq!(temp_storage.n_attributes(), 0); + assert_eq!(length_storage.n_attributes(), 0); +} + +#[test] +fn test_orbit_specific_merges() { + let mut manager = AttrStorageManager::default(); + manager.add_storage::(5); + + // Setup values + manager.set_attribute(0, Temperature::from(20.0)); + manager.set_attribute(1, Temperature::from(30.0)); + + // Test vertex-specific merge + manager.merge_vertex_attributes(2, 0, 1); + + assert!(manager.get_attribute::(2).is_some()); +} + +#[test] +fn test_orbit_specific_splits() { + let mut manager = AttrStorageManager::default(); + manager.add_storage::(5); + + // Setup value + manager.set_attribute(0, Temperature::from(25.0)); + + // Test vertex-specific split + manager.split_vertex_attributes(1, 2, 0); + + assert!(manager.get_attribute::(1).is_some()); + assert!(manager.get_attribute::(2).is_some()); +} // --- unit tests +// transactional manager methods + +fn setup_manager() -> AttrStorageManager { + let mut manager = AttrStorageManager::default(); + manager.add_storage::(10); // Initialize with size 10 + manager +} + +#[test] +fn test_merge_vertex_attributes_transac() { + let manager = setup_manager(); + // Set initial values + manager.set_attribute(0, Temperature::from(20.0)); + manager.set_attribute(1, Temperature::from(30.0)); + + atomically(|trans| manager.merge_vertex_attributes_transac(trans, 2, 0, 1)); + + // Verify merged result + let merged = manager.get_attribute::(2); + assert!(merged.is_some()); + assert_eq!(merged.unwrap(), Temperature::from(25.0)); +} + +#[test] +fn test_split_vertex_attributes_transac() { + let manager = setup_manager(); + + // Set initial value + manager.set_attribute(0, Temperature::from(20.0)); + + atomically(|trans| manager.split_vertex_attributes_transac(trans, 1, 2, 0)); + + // Verify split results + let split1 = manager.get_attribute::(1); + let split2 = manager.get_attribute::(2); + + assert!(split1.is_some()); + assert!(split2.is_some()); + assert_eq!(split1.unwrap().val, 20.0); + assert_eq!(split2.unwrap().val, 20.0); +} + +#[test] +fn test_set_attribute_transac() { + let manager = setup_manager(); + + atomically(|trans| manager.set_attribute_transac(trans, 0, Temperature::from(25.0))); + + let value = manager.get_attribute::(0); + assert!(value.is_some()); + assert_eq!(value.unwrap().val, 25.0); +} + +#[test] +fn test_insert_attribute_transac() { + let manager = setup_manager(); + + atomically(|trans| manager.insert_attribute_transac(trans, 0, Temperature::from(25.0))); + + let value = manager.get_attribute::(0); + assert!(value.is_some()); + assert_eq!(value.unwrap().val, 25.0); +} + +#[test] +fn test_get_attribute_transac() { + let manager = setup_manager(); + + // Set initial value + manager.set_attribute(0, Temperature::from(25.0)); + + let value = atomically(|trans| manager.get_attribute_transac::(trans, 0)); + + assert!(value.is_some()); + assert_eq!(value.unwrap().val, 25.0); +} + +#[test] +fn test_replace_attribute_transac() { + let manager = setup_manager(); + + // Set initial value + manager.set_attribute(0, Temperature::from(25.0)); + + let old_value = + atomically(|trans| manager.replace_attribute_transac(trans, 0, Temperature::from(30.0))); + + assert!(old_value.is_some()); + assert_eq!(old_value.unwrap().val, 25.0); + + let new_value = manager.get_attribute::(0); + assert!(new_value.is_some()); + assert_eq!(new_value.unwrap().val, 30.0); +} + +#[test] +fn test_remove_attribute_transac() { + let manager = setup_manager(); + + // Set initial value + manager.set_attribute(0, Temperature::from(25.0)); + + let removed_value = + atomically(|trans| manager.remove_attribute_transac::(trans, 0)); + + assert!(removed_value.is_some()); + assert_eq!(removed_value.unwrap().val, 25.0); + + let value = manager.get_attribute::(0); + assert!(value.is_none()); +} + +#[test] +fn test_merge_attribute_transac() { + let manager = setup_manager(); + + // Set initial values + manager.set_attribute(0, Temperature::from(20.0)); + manager.set_attribute(1, Temperature::from(30.0)); + + atomically(|trans| manager.merge_attribute_transac::(trans, 2, 0, 1)); + + let merged = manager.get_attribute::(2); + assert!(merged.is_some()); + assert_eq!(merged.unwrap().val, 25.0); // Assuming merge averages values +} + +#[test] +fn test_split_attribute_transac() { + let manager = setup_manager(); + + // Set initial value + manager.set_attribute(0, Temperature::from(20.0)); + + atomically(|trans| manager.split_attribute_transac::(trans, 1, 2, 0)); + + let split1 = manager.get_attribute::(1); + let split2 = manager.get_attribute::(2); + + assert!(split1.is_some()); + assert!(split2.is_some()); + assert_eq!(split1.unwrap().val, 20.0); // Assuming split copies values + assert_eq!(split2.unwrap().val, 20.0); +} + +#[test] +fn test_attribute_operations_with_failed_transaction() { + let manager = setup_manager(); + + // Set initial value + manager.set_attribute(0, Temperature::from(25.0)); + + let _: Option<()> = Transaction::with_control( + |_err| TransactionControl::Abort, + |trans| { + manager.set_attribute_transac(trans, 0, Temperature::from(30.0))?; + manager.insert_attribute_transac(trans, 1, Temperature::from(35.0))?; + + Err(StmError::Failure) + }, + ); + + // Verify original values remained unchanged + let value0 = manager.get_attribute::(0); + let value1 = manager.get_attribute::(1); + + assert!(value0.is_some()); + assert_eq!(value0.unwrap().val, 25.0); + assert!(value1.is_none()); +} + // traits #[test] @@ -435,3 +799,140 @@ fn manager_split_attribute() { assert_eq!(manager.get_attribute(6), Some(Temperature::from(289.0))); assert_eq!(manager.get_attribute::(8), None); } + +// --- parallel + +#[allow(clippy::too_many_lines)] +#[test] +fn manager_ordering() { + loom::model(|| { + // setup manager; slot 0, 1, 2, 3 + let mut manager = AttrStorageManager::default(); + manager.add_storage::(4); + manager.add_storage::(4); + manager.add_storage::(4); + manager.add_storage::(4); + + manager.set_attribute(1, Temperature::from(20.0)); + manager.set_attribute(3, Temperature::from(30.0)); + + manager.set_attribute(1, Length(3.0)); + manager.set_attribute(3, Length(2.0)); + + manager.set_attribute(1, Weight(10)); + manager.set_attribute(3, Weight(15)); + + manager.set_attribute(1, Color(255, 0, 0)); + manager.set_attribute(3, Color(0, 0, 255)); + + let arc = Arc::new(manager); + let c1 = arc.clone(); + let c2 = arc.clone(); + + // we're going to do 2 ops: + // - merge (1, 3) => 2 + // - split 2 =< (2, 3) + // depending on the execution path, attribute values on slots 2 and 3 will vary + // attribute value of slot 1 should be None in any case + + let t1 = loom::thread::spawn(move || { + atomically(|trans| { + c1.merge_vertex_attributes_transac(trans, 2, 1, 3)?; + c1.merge_edge_attributes_transac(trans, 2, 1, 3)?; + c1.merge_face_attributes_transac(trans, 2, 1, 3)?; + Ok(()) + }); + }); + + let t2 = loom::thread::spawn(move || { + atomically(|trans| { + c2.split_vertex_attributes_transac(trans, 2, 3, 2)?; + c2.split_edge_attributes_transac(trans, 2, 3, 2)?; + c2.split_face_attributes_transac(trans, 2, 3, 2)?; + Ok(()) + }); + }); + + t1.join().unwrap(); + t2.join().unwrap(); + + // in both cases + let slot_1_is_empty = arc.get_attribute::(1).is_none() + && arc.get_attribute::(1).is_none() + && arc.get_attribute::(1).is_none() + && arc.get_attribute::(1).is_none(); + assert!(slot_1_is_empty); + + // path 1: merge before split + let p1_2_temp = arc + .get_attribute::(2) + .is_some_and(|val| val == Temperature::from(25.0)); + let p1_3_temp = arc + .get_attribute::(3) + .is_some_and(|val| val == Temperature::from(25.0)); + + let p1_2_weight = arc + .get_attribute::(2) + .is_some_and(|v| v == Weight(13)); + let p1_3_weight = arc + .get_attribute::(3) + .is_some_and(|v| v == Weight(12)); + + let p1_2_len = arc + .get_attribute::(2) + .is_some_and(|v| v == Length(2.5)); + let p1_3_len = arc + .get_attribute::(3) + .is_some_and(|v| v == Length(2.5)); + + let p1_2_col = arc + .get_attribute::(2) + .is_some_and(|v| v == Color(127, 0, 127)); + let p1_3_col = arc + .get_attribute::(3) + .is_some_and(|v| v == Color(127, 0, 127)); + + let p1 = slot_1_is_empty + && p1_2_temp + && p1_3_temp + && p1_2_weight + && p1_3_weight + && p1_2_len + && p1_3_len + && p1_2_col + && p1_3_col; + + // path 2: split before merge + let p2_2_temp = arc + .get_attribute::(2) + .is_some_and(|val| val == Temperature::from(5.0)); + let p2_3_temp = arc.get_attribute::(3).is_none(); + + let p2_2_weight = arc + .get_attribute::(2) + .is_some_and(|v| v == Weight(10)); + let p2_3_weight = arc.get_attribute::(3).is_none(); + + let p2_2_len = arc + .get_attribute::(2) + .is_some_and(|v| v == Length(3.0)); + let p2_3_len = arc.get_attribute::(3).is_none(); + + let p2_2_col = arc + .get_attribute::(2) + .is_some_and(|v| v == Color(255, 0, 0)); + let p2_3_col = arc.get_attribute::(3).is_none(); + + let p2 = slot_1_is_empty + && p2_2_temp + && p2_3_temp + && p2_2_weight + && p2_3_weight + && p2_2_len + && p2_3_len + && p2_2_col + && p2_3_col; + + assert!(p1 || p2); + }); +} diff --git a/honeycomb-core/src/cmap/dim2/link_and_sew.rs b/honeycomb-core/src/cmap/dim2/link_and_sew.rs index 48a2bf9b1..6c96fa32b 100644 --- a/honeycomb-core/src/cmap/dim2/link_and_sew.rs +++ b/honeycomb-core/src/cmap/dim2/link_and_sew.rs @@ -442,18 +442,13 @@ impl CMap2 { self.one_unlink_core(trans, lhs_dart_id)?; // split vertices & attributes from the old ID to the new ones // FIXME: VertexIdentifier should be cast to DartIdentifier - self.vertices.split_core( - trans, - self.vertex_id(b2lhs_dart_id), - self.vertex_id(rhs_dart_id), - vid_old, - )?; - self.attributes.split_vertex_attributes_transac( - trans, - self.vertex_id(b2lhs_dart_id), - self.vertex_id(rhs_dart_id), - vid_old, - )?; + let (new_lhs, new_rhs) = ( + self.vertex_id_transac(trans, b2lhs_dart_id)?, + self.vertex_id_transac(trans, rhs_dart_id)?, + ); + self.vertices.split_core(trans, new_lhs, new_rhs, vid_old)?; + self.attributes + .split_vertex_attributes_transac(trans, new_lhs, new_rhs, vid_old)?; } Ok(()) }); @@ -499,34 +494,32 @@ impl CMap2 { (true, false) => { // fetch IDs before topology update let eid_old = self.edge_id(lhs_dart_id); - let rhs_vid_old = self.vertex_id(rhs_dart_id); + let lhs_vid_old = self.vertex_id(lhs_dart_id); // update the topology self.two_unlink(lhs_dart_id); // split vertex & attributes from the old ID to the new ones // FIXME: VertexIdentifier should be cast to DartIdentifier self.attributes .split_edge_attributes(lhs_dart_id, rhs_dart_id, eid_old); - self.attributes.split_vertex_attributes( - self.vertex_id(b1lhs_dart_id), - self.vertex_id(rhs_dart_id), - rhs_vid_old, - ); + let (new_lv_lhs, new_lv_rhs) = + (self.vertex_id(lhs_dart_id), self.vertex_id(b1rhs_dart_id)); + self.attributes + .split_vertex_attributes(new_lv_lhs, new_lv_rhs, lhs_vid_old); } (false, true) => { // fetch IDs before topology update let eid_old = self.edge_id(lhs_dart_id); - let lhs_vid_old = self.vertex_id(lhs_dart_id); + let rhs_vid_old = self.vertex_id(rhs_dart_id); // update the topology self.two_unlink(lhs_dart_id); // split vertex & attributes from the old ID to the new ones // FIXME: VertexIdentifier should be cast to DartIdentifier self.attributes .split_edge_attributes(lhs_dart_id, rhs_dart_id, eid_old); - self.attributes.split_vertex_attributes( - self.vertex_id(lhs_dart_id), - self.vertex_id(b1rhs_dart_id), - lhs_vid_old, - ); + let (new_rv_lhs, new_rv_rhs) = + (self.vertex_id(b1lhs_dart_id), self.vertex_id(rhs_dart_id)); + self.attributes + .split_vertex_attributes(new_rv_lhs, new_rv_rhs, rhs_vid_old); } (false, false) => { // fetch IDs before topology update @@ -539,16 +532,14 @@ impl CMap2 { // FIXME: VertexIdentifier should be cast to DartIdentifier self.attributes .split_edge_attributes(lhs_dart_id, rhs_dart_id, eid_old); - self.attributes.split_vertex_attributes( - self.vertex_id(b1lhs_dart_id), - self.vertex_id(rhs_dart_id), - rhs_vid_old, - ); - self.attributes.split_vertex_attributes( - self.vertex_id(lhs_dart_id), - self.vertex_id(b1rhs_dart_id), - lhs_vid_old, - ); + let (new_lv_lhs, new_lv_rhs) = + (self.vertex_id(lhs_dart_id), self.vertex_id(b1rhs_dart_id)); + let (new_rv_lhs, new_rv_rhs) = + (self.vertex_id(b1lhs_dart_id), self.vertex_id(rhs_dart_id)); + self.attributes + .split_vertex_attributes(new_lv_lhs, new_lv_rhs, lhs_vid_old); + self.attributes + .split_vertex_attributes(new_rv_lhs, new_rv_rhs, rhs_vid_old); } } } @@ -578,7 +569,7 @@ impl CMap2 { (true, false) => { // fetch IDs before topology update let eid_old = self.edge_id_transac(trans, lhs_dart_id)?; - let rhs_vid_old = self.vertex_id_transac(trans, rhs_dart_id)?; + let lhs_vid_old = self.vertex_id_transac(trans, lhs_dart_id)?; // update the topology self.two_unlink_core(trans, lhs_dart_id)?; // split vertex & attributes from the old ID to the new ones @@ -589,17 +580,21 @@ impl CMap2 { rhs_dart_id, eid_old, )?; + let (new_lv_lhs, new_lv_rhs) = ( + self.vertex_id_transac(trans, lhs_dart_id)?, + self.vertex_id_transac(trans, b1rhs_dart_id)?, + ); self.attributes.split_vertex_attributes_transac( trans, - self.vertex_id(b1lhs_dart_id), - self.vertex_id(rhs_dart_id), - rhs_vid_old, + new_lv_lhs, + new_lv_rhs, + lhs_vid_old, )?; } (false, true) => { // fetch IDs before topology update let eid_old = self.edge_id_transac(trans, lhs_dart_id)?; - let lhs_vid_old = self.vertex_id_transac(trans, lhs_dart_id)?; + let rhs_vid_old = self.vertex_id_transac(trans, rhs_dart_id)?; // update the topology self.two_unlink_core(trans, lhs_dart_id)?; // split vertex & attributes from the old ID to the new ones @@ -610,11 +605,15 @@ impl CMap2 { rhs_dart_id, eid_old, )?; + let (new_rv_lhs, new_rv_rhs) = ( + self.vertex_id_transac(trans, b1lhs_dart_id)?, + self.vertex_id_transac(trans, rhs_dart_id)?, + ); self.attributes.split_vertex_attributes_transac( trans, - self.vertex_id(lhs_dart_id), - self.vertex_id(b1rhs_dart_id), - lhs_vid_old, + new_rv_lhs, + new_rv_rhs, + rhs_vid_old, )?; } (false, false) => { @@ -632,17 +631,25 @@ impl CMap2 { rhs_dart_id, eid_old, )?; + let (new_lv_lhs, new_lv_rhs) = ( + self.vertex_id_transac(trans, lhs_dart_id)?, + self.vertex_id_transac(trans, b1rhs_dart_id)?, + ); + let (new_rv_lhs, new_rv_rhs) = ( + self.vertex_id_transac(trans, b1lhs_dart_id)?, + self.vertex_id_transac(trans, rhs_dart_id)?, + ); self.attributes.split_vertex_attributes_transac( trans, - self.vertex_id(b1lhs_dart_id), - self.vertex_id(rhs_dart_id), - rhs_vid_old, + new_lv_lhs, + new_lv_rhs, + lhs_vid_old, )?; self.attributes.split_vertex_attributes_transac( trans, - self.vertex_id(lhs_dart_id), - self.vertex_id(b1rhs_dart_id), - lhs_vid_old, + new_rv_lhs, + new_rv_rhs, + rhs_vid_old, )?; } } diff --git a/honeycomb-core/src/cmap/dim2/tests.rs b/honeycomb-core/src/cmap/dim2/tests.rs index 4bbc81bb3..8f50c58d9 100644 --- a/honeycomb-core/src/cmap/dim2/tests.rs +++ b/honeycomb-core/src/cmap/dim2/tests.rs @@ -1,6 +1,10 @@ // ------ IMPORTS -use crate::prelude::{CMap2, CMapBuilder, Orbit2, OrbitPolicy, Vertex2}; +use crate::{ + attributes::AttrSparseVec, + cmap::VertexIdentifier, + prelude::{AttributeBind, AttributeUpdate, CMap2, CMapBuilder, Orbit2, OrbitPolicy, Vertex2}, +}; // ------ CONTENT @@ -323,3 +327,122 @@ fn io_write() { assert!(res.contains("2 7 8")); assert!(res.contains("2 8 3")); } + +// --- PARALLEL + +#[derive(Debug, Clone, Copy, Default)] +struct Weight(pub u32); + +impl AttributeUpdate for Weight { + fn merge(attr1: Self, attr2: Self) -> Self { + Self(attr1.0 + attr2.0) + } + + fn split(attr: Self) -> (Self, Self) { + // adding the % to keep things conservative + (Weight(attr.0 / 2 + attr.0 % 2), Weight(attr.0 / 2)) + } +} + +impl AttributeBind for Weight { + type StorageType = AttrSparseVec; + type IdentifierType = VertexIdentifier; + const BIND_POLICY: OrbitPolicy = OrbitPolicy::Vertex; +} + +#[test] +fn sew_ordering() { + loom::model(|| { + // setup the map + let map: CMap2 = CMapBuilder::default().n_darts(5).build().unwrap(); + map.two_link(1, 2); + map.one_link(4, 5); + map.insert_vertex(2, Vertex2(1.0, 1.0)); + map.insert_vertex(3, Vertex2(1.0, 2.0)); + map.insert_vertex(5, Vertex2(2.0, 2.0)); + let arc = loom::sync::Arc::new(map); + let (m1, m2) = (arc.clone(), arc.clone()); + + // we're going to do to sew ops: + // - 1-sew 1 to 3 (t1) + // - 2-sew 3 to 4 (t2) + // this will result in a single vertex being define, of ID 2 + // depending on the order of execution of the sews, the value may change + // 1-sew before 2-sew: (1.5, 1.75) + // 2-sew before 1-sew: (1.25, 1.5) + + let t1 = loom::thread::spawn(move || { + m1.atomically_one_sew(1, 3); + }); + + let t2 = loom::thread::spawn(move || { + m2.atomically_two_sew(3, 4); + }); + + t1.join().unwrap(); + t2.join().unwrap(); + + // all path should result in the same topological result here + assert!(arc.vertex(2).is_some()); + assert!(arc.vertex(3).is_none()); + assert!(arc.vertex(5).is_none()); + assert_eq!(Orbit2::new(arc.as_ref(), OrbitPolicy::Vertex, 2).count(), 3); + + // the v2 can have two values though + let path1 = arc.vertex(2) == Some(Vertex2(1.5, 1.75)); + let path2 = arc.vertex(2) == Some(Vertex2(1.25, 1.5)); + assert!(path1 || path2); + }); +} + +#[test] +fn unsew_ordering() { + loom::model(|| { + // setup the map + let map: CMap2 = CMapBuilder::default() + .n_darts(5) + .add_attribute::() + .build() + .unwrap(); + map.two_link(1, 2); + map.two_link(3, 4); + map.one_link(1, 3); + map.one_link(4, 5); + map.insert_vertex(2, Vertex2(0.0, 0.0)); + map.insert_attribute(2, Weight(33)); + let arc = loom::sync::Arc::new(map); + let (m1, m2) = (arc.clone(), arc.clone()); + + // we're going to do to sew ops: + // - 1-unsew 1 and 3 (t1) + // - 2-unsew 3 and 4 (t2) + // this will result in 3 different weights, defined on IDs 2, 3 and 5 + // depending on the order of execution, the final weights will take the following values: + // 1-unsew before 2-unsew: (W2, W3, W5) = (17, 8, 8) + // 2-unsew before 1-unsew: (W2, W3, W5) = (9, 8, 16) + + let t1 = loom::thread::spawn(move || { + m1.atomically_one_unsew(1); + }); + + let t2 = loom::thread::spawn(move || { + m2.atomically_two_unsew(3); + }); + + t1.join().unwrap(); + t2.join().unwrap(); + + // all path should result in the same topological result here + assert!(arc.get_attribute::(2).is_some()); + assert!(arc.get_attribute::(3).is_some()); + assert!(arc.get_attribute::(5).is_some()); + let w2 = arc.get_attribute::(2).unwrap(); + let w3 = arc.get_attribute::(3).unwrap(); + let w5 = arc.get_attribute::(5).unwrap(); + + // check scenarios + let path1 = w2.0 == 17 && w3.0 == 8 && w5.0 == 8; + let path2 = w2.0 == 9 && w3.0 == 8 && w5.0 == 16; + assert!(path1 || path2); + }); +}