Repository: Pometry/Raphtory (language: python)

Issue:
Odd behaviour of property repr across layers in Python
Sometimes the printing of an edge has some incorrect behaviours when different layers are involved. Here is a small example

```python
# event graph with two layers and properties
G = Graph()
G.add_edge(1, "A", "B", layer = "layer 1", properties={"greeting":"howdy"})
G.add_edge(2, "A", "B", layer = "layer 2", properties={"greeting":"yo"})
print(G.edge("A","B"))
print(G.edge("A","B").explode())
```

The first print gives
```
Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: yo, greeting: yo}, layer(s)=[layer 1, layer 2])
```
Maybe `greeting: yo and greeting: howdy` would be reasonable or just `greeting:yo` if just the latest added property is displayed, but not two `yo`s.

The second print looks correct:
```
Edges(Edge(source=A, target=B, earliest_time=1, latest_time=1, properties={greeting: howdy}, layer(s)=[layer 1]), Edge(source=A, target=B, earliest_time=2, latest_time=2, properties={greeting: yo}, layer(s)=[layer 2]))
```
Odd behaviour of property repr across layers in Python
Sometimes the printing of an edge has some incorrect behaviours when different layers are involved. Here is a small example

```python
# event graph with two layers and properties
G = Graph()
G.add_edge(1, "A", "B", layer = "layer 1", properties={"greeting":"howdy"})
G.add_edge(2, "A", "B", layer = "layer 2", properties={"greeting":"yo"})
print(G.edge("A","B"))
print(G.edge("A","B").explode())
```

The first print gives
```
Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: yo, greeting: yo}, layer(s)=[layer 1, layer 2])
```
Maybe `greeting: yo and greeting: howdy` would be reasonable or just `greeting:yo` if just the latest added property is displayed, but not two `yo`s.

The second print looks correct:
```
Edges(Edge(source=A, target=B, earliest_time=1, latest_time=1, properties={greeting: howdy}, layer(s)=[layer 1]), Edge(source=A, target=B, earliest_time=2, latest_time=2, properties={greeting: yo}, layer(s)=[layer 2]))
```


Discussion / hints:
Possible follow up is that if the latest property value for each layer is returned for a non-exploded edge, it could be shown as something like
```
Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: howdy (layer layer 1), greeting: yo (layer layer 2}, layer(s)=[layer 1, layer 2])
```
so that it's clear where each property item is coming from. 
Possible follow up is that if the latest property value for each layer is returned for a non-exploded edge, it could be shown as something like
```
Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: howdy (layer layer 1), greeting: yo (layer layer 2}, layer(s)=[layer 1, layer 2])
```
so that it's clear where each property item is coming from. 

Fix patch:
diff --git a/raphtory/src/core/storage/raw_edges.rs b/raphtory/src/core/storage/raw_edges.rs
index 84dbbc7c8..f3fae12a3 100644
--- a/raphtory/src/core/storage/raw_edges.rs
+++ b/raphtory/src/core/storage/raw_edges.rs
@@ -6,6 +6,7 @@ use crate::{
     },
     db::api::storage::graph::edges::edge_storage_ops::{EdgeStorageOps, MemEdge},
 };
+use itertools::Itertools;
 use lock_api::ArcRwLockReadGuard;
 use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
 use raphtory_api::core::{entities::EID, storage::timeindex::TimeIndexEntry};
@@ -292,7 +293,9 @@ impl<'a> EdgeRGuard<'a> {
             Box::new(
                 self.guard
                     .props_iter(self.offset)
-                    .flat_map(|(_, layer)| layer.temporal_prop_ids()),
+                    .map(|(_, layer)| layer.temporal_prop_ids())
+                    .kmerge()
+                    .dedup(),
             )
         }
     }
diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs
index 3c4028e56..3b28576ee 100644
--- a/raphtory/src/db/api/view/graph.rs
+++ b/raphtory/src/db/api/view/graph.rs
@@ -779,4 +779,25 @@ mod test_materialize {
         g.add_edge(0, 1, 2, props_0.clone(), None).unwrap();
         assert!(g.add_edge(1, 1, 2, props_1.clone(), None).is_err());
     }
+
+    #[test]
+    fn test_edge_layer_properties() {
+        let g = Graph::new();
+        g.add_edge(1, "A", "B", [("greeting", "howdy")], Some("layer 1"))
+            .unwrap();
+        g.add_edge(2, "A", "B", [("greeting", "ola")], Some("layer 2"))
+            .unwrap();
+        g.add_edge(2, "A", "B", [("greeting", "hello")], Some("layer 2"))
+            .unwrap();
+        g.add_edge(3, "A", "B", [("greeting", "namaste")], Some("layer 3"))
+            .unwrap();
+
+        let edge_ab = g.edge("A", "B").unwrap();
+        let props = edge_ab
+            .properties()
+            .iter()
+            .map(|(k, v)| (k.to_string(), v.to_string()))
+            .collect::<Vec<_>>();
+        assert_eq!(props, vec![("greeting".to_string(), "namaste".to_string())]);
+    }
 }
diff --git a/raphtory/src/python/types/macros/trait_impl/edgeviewops.rs b/raphtory/src/python/types/macros/trait_impl/edgeviewops.rs
index 278777b82..34324e033 100644
--- a/raphtory/src/python/types/macros/trait_impl/edgeviewops.rs
+++ b/raphtory/src/python/types/macros/trait_impl/edgeviewops.rs
@@ -32,13 +32,14 @@ macro_rules! impl_edgeviewops {
                 self.$field.nbr()
             }
 
-            /// Explodes an edge and returns all instances it had been updated as seperate edges
+            /// Explodes returns an edge object for each update within the original edge.
             fn explode(
                 &self,
             ) -> <$base_type as $crate::db::api::view::EdgeViewOps<'static>>::Exploded {
                 self.$field.explode()
             }
 
+            /// Explode layers returns an edge object for each layer within the original edge. These new edge object contains only updates from respective layers.
             fn explode_layers(
                 &self,
             ) -> <$base_type as $crate::db::api::view::EdgeViewOps<'static>>::Exploded {


Test patch:
diff --git a/python/tests/test_graphdb/test_graphdb.py b/python/tests/test_graphdb/test_graphdb.py
index bed24790c..071803ab9 100644
--- a/python/tests/test_graphdb/test_graphdb.py
+++ b/python/tests/test_graphdb/test_graphdb.py
@@ -3047,6 +3047,16 @@ def test_create_node_graph_with_deletion():
     assert "Node already exists" in str(excinfo.value)
 
 
+def test_edge_layer_properties():
+    g = Graph()
+    g.add_edge(1, "A", "B", properties={"greeting": "howdy"}, layer="layer 1")
+    g.add_edge(2, "A", "B", properties={"greeting": "hello"}, layer="layer 2")
+    g.add_edge(3, "A", "B", properties={"greeting": "ola"}, layer="layer 2")
+    g.add_edge(3, "A", "B", properties={"greeting": "namaste"}, layer="layer 3")
+
+    assert g.edge("A", "B").properties == {"greeting": "namaste"}
+
+
 @fixture
 def datadir(tmpdir, request):
     filename = request.module.__file__
diff --git a/python/tests/test_repr.py b/python/tests/test_repr.py
index 5978e214a..703aabc20 100644
--- a/python/tests/test_repr.py
+++ b/python/tests/test_repr.py
@@ -45,7 +45,7 @@ def test_layers_and_props(self):
         G = Graph()
         G.add_edge(1, "A", "B", layer="layer 1", properties={"greeting": "howdy"})
         G.add_edge(2, "A", "B", layer="layer 2", properties={"greeting": "yo"})
-        expected_out = "Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: yo, greeting: yo}, layer(s)=[layer 1, layer 2])\n"
+        expected_out = "Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: yo}, layer(s)=[layer 1, layer 2])\n"
         with patch("sys.stdout", new=StringIO()) as fake_out:
             print(G.edge("A", "B"))
             self.assertEqual(fake_out.getvalue(), expected_out)
@@ -60,7 +60,7 @@ def layers_and_non_layers(self):
         G = Graph()
         G.add_edge(1, "A", "B", layer="layer 1", properties={"greeting": "howdy"})
         G.add_edge(2, "A", "B", properties={"greeting": "yo"})
-        expected_out = "Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: yo, greeting: yo}, layer(s)=[_default, layer 1])\n"
+        expected_out = "Edge(source=A, target=B, earliest_time=1, latest_time=2, properties={greeting: yo}, layer(s)=[_default, layer 1])\n"
         with patch("sys.stdout", new=StringIO()) as fake_out:
             print(G.edge("A", "B"))
             self.assertEqual(fake_out.getvalue(), expected_out)
@@ -72,7 +72,7 @@ def test_persistent_graph(self):
         G.delete_edge(5, "A", "B", layer="layer 1")
         G.add_edge(2, "A", "B", layer="layer 2", properties={"greeting": "yo"})
         G.delete_edge(6, "A", "B", layer="layer 2")
-        expected_out = "Edge(source=A, target=B, earliest_time=1, latest_time=6, properties={greeting: yo, greeting: yo}, layer(s)=[layer 1, layer 2])\n"
+        expected_out = "Edge(source=A, target=B, earliest_time=1, latest_time=6, properties={greeting: yo}, layer(s)=[layer 1, layer 2])\n"
         with patch("sys.stdout", new=StringIO()) as fake_out:
             print(G.edge("A", "B"))
             self.assertEqual(fake_out.getvalue(), expected_out)