From f9b879971c5f35b2405185525c8c1e5ea6bec6e3 Mon Sep 17 00:00:00 2001 From: ljeub-pometry <97447091+ljeub-pometry@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:37:44 +0100 Subject: [PATCH] Feature/improve time ops (#1371) * Add before and after to TimeOps and change at so it only includes events happening at that point in time (this should fix the semantics for the GraphWithDeletions so the partial_windows workaround is no longer needed) * Revert "Added 'hard deletion' semantics (#1348)" f72f97cf436788a0791fcb854a7e35b25b95654f * fix edge alive check * at methods in python and fix tests * enable the warning comment for pr * fix benchmark action * fix the documentation for time ops * update the impl_timeops docstring * add docstring for before and after on Edges --------- Co-authored-by: Haaroon Y Co-authored-by: Ben Steer --- .github/workflows/benchmark.yml | 2 + python/tests/test_graphdb.py | 95 ++++-- .../metrics/clustering_coefficient.rs | 4 +- .../src/algorithms/motifs/triplet_count.rs | 2 +- raphtory/src/db/api/view/time.rs | 27 +- raphtory/src/db/graph/graph.rs | 60 +++- raphtory/src/db/graph/vertex.rs | 16 +- raphtory/src/db/graph/views/deletion_graph.rs | 241 ++++++------- raphtory/src/graph_loader/mod.rs | 8 +- raphtory/src/python/graph/edge.rs | 162 +++------ .../src/python/graph/graph_with_deletions.rs | 7 - raphtory/src/python/graph/vertex.rs | 316 +----------------- raphtory/src/python/graph/views/graph_view.rs | 115 +------ raphtory/src/python/types/macros/mod.rs | 3 + raphtory/src/python/types/macros/timeops.rs | 140 ++++++++ 15 files changed, 466 insertions(+), 732 deletions(-) create mode 100644 raphtory/src/python/types/macros/timeops.rs diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 475c18ba32..7e40cb89c5 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -74,6 +74,8 @@ jobs: auto-push: false # Show alert with commit comment on detecting possible performance regression alert-threshold: '200%' + comment-on-alert: true + github-token: ${{ secrets.GITHUB_TOKEN }} fail-on-alert: false save-data-file: false diff --git a/python/tests/test_graphdb.py b/python/tests/test_graphdb.py index 5cfb92ae35..e0c66d1f44 100644 --- a/python/tests/test_graphdb.py +++ b/python/tests/test_graphdb.py @@ -280,9 +280,9 @@ def history_test(key, value): def time_history_test(time, key, value): if value is None: - assert g.at(time).properties.temporal.get(key) is None + assert g.before(time+1).properties.temporal.get(key) is None else: - assert g.at(time).properties.temporal.get(key).items() == value + assert g.before(time+1).properties.temporal.get(key).items() == value time_history_test(2, "prop 6", [(1, False), (2, True)]) time_history_test(1, "static prop", None) @@ -325,7 +325,7 @@ def property_test(key, value): "prop 5": "world", "prop 6": True, } - assert g.at(2).properties.as_dict() == { + assert g.before(3).properties.as_dict() == { "prop 1": 1, "prop 2": "hi", "prop 3": True, @@ -341,7 +341,7 @@ def property_test(key, value): "prop 6": [(1, False), (2, True)], } - assert g.at(2).properties.temporal.histories() == { + assert g.before(3).properties.temporal.histories() == { "prop 4": [(1, 11)], "prop 5": [(1, "world")], "prop 6": [(1, False), (2, True)], @@ -356,14 +356,14 @@ def property_test(key, value): expected_names_no_static = sorted(["prop 4", "prop 5", "prop 6"]) assert sorted(g.properties.temporal.keys()) == expected_names_no_static - assert sorted(g.at(1).properties.temporal.keys()) == expected_names_no_static + assert sorted(g.before(2).properties.temporal.keys()) == expected_names_no_static # testing has_property assert "prop 4" in g.properties assert "prop 7" not in g.properties - assert "prop 7" not in g.at(1).properties + assert "prop 7" not in g.before(2).properties assert "prop 1" in g.properties - assert "prop 2" in g.at(1).properties + assert "prop 2" in g.before(2).properties assert "static prop" not in g.properties.constant @@ -415,7 +415,7 @@ def time_history_test(time, key, value): time_history_test(1, "static prop", None) def time_static_property_test(time, key, value): - gg = g.at(time) + gg = g.before(time+1) if value is None: assert gg.vertex(1).properties.constant.get(key) is None assert gg.vertices.properties.constant.get(key) is None @@ -442,7 +442,7 @@ def static_property_test(key, value): # testing property def time_property_test(time, key, value): - gg = g.at(time) + gg = g.before(time+1) if value is None: assert gg.vertex(1).properties.get(key) is None assert gg.vertices.properties.get(key) is None @@ -523,21 +523,21 @@ def no_static_property_test(key, value): "prop 4": [[True]], } - assert g.at(2).vertex(1).properties == { + assert g.before(3).vertex(1).properties == { "prop 1": 2, "prop 4": False, "prop 2": 0.6, "static prop": 123, "prop 3": "hi", } - assert g.at(2).vertices.properties == { + assert g.before(3).vertices.properties == { "prop 1": [2], "prop 4": [False], "prop 2": [0.6], "static prop": [123], "prop 3": ["hi"], } - assert g.at(2).vertices.out_neighbours.properties == { + assert g.before(3).vertices.out_neighbours.properties == { "prop 1": [[2]], "prop 4": [[False]], "prop 2": [[0.6]], @@ -567,17 +567,16 @@ def no_static_property_test(key, value): assert g.at(2).vertex(1).properties.temporal == { "prop 2": [(2, 0.6)], - "prop 4": [(1, True), (2, False)], - "prop 1": [(1, 1), (2, 2)], - "prop 3": [(1, "hi")], + "prop 4": [(2, False)], + "prop 1": [(2, 2)], } - assert g.at(2).vertices.properties.temporal == { + assert g.before(3).vertices.properties.temporal == { "prop 2": [[(2, 0.6)]], "prop 4": [[(1, True), (2, False)]], "prop 1": [[(1, 1), (2, 2)]], "prop 3": [[(1, "hi")]], } - assert g.at(2).vertices.out_neighbours.properties.temporal == { + assert g.before(3).vertices.out_neighbours.properties.temporal == { "prop 2": [[[(2, 0.6)]]], "prop 4": [[[(1, True), (2, False)]]], "prop 1": [[[(1, 1), (2, 2)]]], @@ -679,7 +678,7 @@ def test_edge_properties(): assert g.at(1).edge(1, 2).properties.temporal.get("static prop") is None assert g.at(1).edge(1, 2).properties.constant.get("static prop") == 123 - assert g.at(100).edge(1, 2).properties.constant.get("static prop") == 123 + assert g.before(101).edge(1, 2).properties.constant.get("static prop") == 123 assert g.edge(1, 2).properties.constant.get("static prop") == 123 assert g.edge(1, 2).properties.constant.get("prop 4") is None @@ -707,7 +706,7 @@ def test_edge_properties(): "prop 4": True, } - assert g.at(2).edge(1, 2).properties == { + assert g.before(3).edge(1, 2).properties == { "prop 1": 2, "prop 4": False, "prop 2": 0.6, @@ -725,9 +724,14 @@ def test_edge_properties(): assert g.at(2).edge(1, 2).properties.temporal == { "prop 2": [(2, 0.6)], - "prop 4": [(1, True), (2, False)], - "prop 1": [(1, 1), (2, 2)], - "prop 3": [(1, "hi")], + "prop 4": [(2, False)], + "prop 1": [(2, 2)], + } + + assert g.after(2).edge(1, 2).properties.temporal == { + "prop 2": [(3, 0.9)], + "prop 3": [(3, "hello")], + "prop 4": [(3, True)], } # testing property names @@ -875,13 +879,21 @@ def test_save_load_graph(): def test_graph_at(): g = create_graph() - view = g.at(2) + view = g.at(1) + assert view.vertex(1).degree() == 2 + assert view.vertex(2).degree() == 1 + + view = g.before(3) assert view.vertex(1).degree() == 3 assert view.vertex(3).degree() == 1 - view = g.at(7) + view = g.before(8) assert view.vertex(3).degree() == 2 + view = g.after(6) + assert view.vertex(2).degree() == 1 + assert view.vertex(3).degree() == 1 + def test_add_node_string(): g = Graph() @@ -917,7 +929,7 @@ def test_all_neighbours_window(): g.add_edge(3, 3, 2, {}) g.add_edge(4, 2, 4, {}) - view = g.at(2) + view = g.before(3) v = view.vertex(2) assert list(v.window(0, 2).in_neighbours.id) == [1] assert list(v.window(0, 2).out_neighbours.id) == [3] @@ -934,7 +946,7 @@ def test_all_degrees_window(): g.add_edge(4, 2, 4, {}) g.add_edge(5, 2, 1, {}) - view = g.at(4) + view = g.before(5) v = view.vertex(2) assert v.window(0, 4).in_degree() == 3 assert v.window(start=2).in_degree() == 2 @@ -957,7 +969,7 @@ def test_all_edge_window(): g.add_edge(4, 2, 4, {}) g.add_edge(5, 2, 1, {}) - view = g.at(4) + view = g.before(5) v = view.vertex(2) assert sorted(v.window(0, 4).in_edges.src.id) == [1, 3, 4] assert sorted(v.window(end=4).in_edges.src.id) == [1, 3, 4] @@ -1012,8 +1024,7 @@ def test_triplet_count(): g.add_edge(0, 2, 3, {}) g.add_edge(0, 3, 1, {}) - v = g.at(1) - assert algorithms.triplet_count(v) == 3 + assert algorithms.triplet_count(g) == 3 def test_global_clustering_coeffficient(): @@ -1026,8 +1037,8 @@ def test_global_clustering_coeffficient(): g.add_edge(0, 4, 1, {}) g.add_edge(0, 5, 2, {}) - v = g.at(1) - assert algorithms.global_clustering_coefficient(v) == 0.5454545454545454 + assert algorithms.global_clustering_coefficient(g) == 0.5454545454545454 + assert algorithms.global_clustering_coefficient(g.at(0)) == 0.5454545454545454 def test_edge_time_apis(): @@ -1082,8 +1093,13 @@ def test_edge_earliest_latest_time(): assert list(g.vertex(1).edges.earliest_time) == [0, 0] assert list(g.vertex(1).edges.latest_time) == [2, 2] - assert list(g.vertex(1).at(1).edges.earliest_time) == [0, 0] + assert list(g.vertex(1).at(1).edges.earliest_time) == [1, 1] + assert list(g.vertex(1).before(1).edges.earliest_time) == [0, 0] + assert list(g.vertex(1).after(1).edges.earliest_time) == [2, 2] assert list(g.vertex(1).at(1).edges.latest_time) == [1, 1] + assert list(g.vertex(1).before(1).edges.latest_time) == [0, 0] + assert list(g.vertex(1).after(1).edges.latest_time) == [2, 2] + def test_vertex_earliest_time(): @@ -1093,9 +1109,14 @@ def test_vertex_earliest_time(): g.add_vertex(2, 1, {}) view = g.at(1) - assert view.vertex(1).earliest_time == 0 + assert view.vertex(1).earliest_time == 1 assert view.vertex(1).latest_time == 1 - view = g.at(3) + + view = g.after(0) + assert view.vertex(1).earliest_time == 1 + assert view.vertex(1).latest_time == 2 + + view = g.before(3) assert view.vertex(1).earliest_time == 0 assert view.vertex(1).latest_time == 2 @@ -1227,8 +1248,8 @@ def test_lotr_edge_history(): 31445, 32656, ] - assert g.at(1000).edge("Frodo", "Gandalf").history() == [329, 555, 861] - assert g.edge("Frodo", "Gandalf").at(1000).history() == [329, 555, 861] + assert g.before(1000).edge("Frodo", "Gandalf").history() == [329, 555, 861] + assert g.edge("Frodo", "Gandalf").before(1000).history() == [329, 555, 861] assert g.window(100, 1000).edge("Frodo", "Gandalf").history() == [329, 555, 861] assert g.edge("Frodo", "Gandalf").window(100, 1000).history() == [329, 555, 861] @@ -1316,7 +1337,7 @@ def test_window_size(): g.add_vertex(1, 1) g.add_vertex(4, 4) - assert g.window_size() == 4 + assert g.window_size == 4 def test_time_index(): diff --git a/raphtory/src/algorithms/metrics/clustering_coefficient.rs b/raphtory/src/algorithms/metrics/clustering_coefficient.rs index 2717cd1527..e3852e2e83 100644 --- a/raphtory/src/algorithms/metrics/clustering_coefficient.rs +++ b/raphtory/src/algorithms/metrics/clustering_coefficient.rs @@ -91,9 +91,7 @@ mod cc_test { graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); } - let graph_at = graph.at(1); - - let results = clustering_coefficient(&graph_at); + let results = clustering_coefficient(&graph); assert_eq!(results, 0.3); } } diff --git a/raphtory/src/algorithms/motifs/triplet_count.rs b/raphtory/src/algorithms/motifs/triplet_count.rs index 2c6e51471c..244c2b4b45 100644 --- a/raphtory/src/algorithms/motifs/triplet_count.rs +++ b/raphtory/src/algorithms/motifs/triplet_count.rs @@ -159,7 +159,7 @@ mod triplet_test { graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); } let exp_triplet_count = 20; - let results = triplet_count(&graph.at(1), None); + let results = triplet_count(&graph, None); assert_eq!(results, exp_triplet_count); } diff --git a/raphtory/src/db/api/view/time.rs b/raphtory/src/db/api/view/time.rs index 5502622f0b..c5101d76a4 100644 --- a/raphtory/src/db/api/view/time.rs +++ b/raphtory/src/db/api/view/time.rs @@ -21,14 +21,31 @@ pub trait TimeOps { /// Create a view including all events between `start` (inclusive) and `end` (exclusive) fn window(&self, start: T, end: T) -> Self::WindowedViewType; - /// Create a view including all events until `end` (inclusive) - fn at(&self, end: T) -> Self::WindowedViewType { + /// Create a view that only includes events at `time` + fn at(&self, time: T) -> Self::WindowedViewType { + let start = time.into_time(); + self.window(start, start.saturating_add(1)) + } + + /// Create a view that only includes events after `start` (exclusive) + fn after(&self, start: T) -> Self::WindowedViewType { + let start = start.into_time().saturating_add(1); + let end = self.end().unwrap_or(start.saturating_add(1)); + if end < start { + self.window(start, start) + } else { + self.window(start, end) + } + } + + /// Create a view that only includes events before `end` (exclusive) + fn before(&self, end: T) -> Self::WindowedViewType { let end = end.into_time(); let start = self.start().unwrap_or(end); - if start > end { - self.window(end, end.saturating_add(1)) + if end < start { + self.window(end, end) } else { - self.window(start, end.saturating_add(1)) + self.window(start, end) } } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index bbeeab39d8..7c0a25b957 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -782,11 +782,23 @@ mod db_tests { assert_eq!(res, 2); res = g.at(1).edge(1, 2).unwrap().earliest_time().unwrap(); + assert_eq!(res, 1); + + res = g.before(1).edge(1, 2).unwrap().earliest_time().unwrap(); assert_eq!(res, 0); + res = g.after(1).edge(1, 2).unwrap().earliest_time().unwrap(); + assert_eq!(res, 2); + res = g.at(1).edge(1, 2).unwrap().latest_time().unwrap(); assert_eq!(res, 1); + res = g.before(1).edge(1, 2).unwrap().latest_time().unwrap(); + assert_eq!(res, 0); + + res = g.after(1).edge(1, 2).unwrap().latest_time().unwrap(); + assert_eq!(res, 2); + let res_list: Vec = g .vertex(1) .unwrap() @@ -813,8 +825,28 @@ mod db_tests { .earliest_time() .flatten() .collect(); + assert_eq!(res_list, vec![1, 1]); + + let res_list: Vec = g + .vertex(1) + .unwrap() + .before(1) + .edges() + .earliest_time() + .flatten() + .collect(); assert_eq!(res_list, vec![0, 0]); + let res_list: Vec = g + .vertex(1) + .unwrap() + .after(1) + .edges() + .earliest_time() + .flatten() + .collect(); + assert_eq!(res_list, vec![2, 2]); + let res_list: Vec = g .vertex(1) .unwrap() @@ -824,6 +856,26 @@ mod db_tests { .flatten() .collect(); assert_eq!(res_list, vec![1, 1]); + + let res_list: Vec = g + .vertex(1) + .unwrap() + .before(1) + .edges() + .latest_time() + .flatten() + .collect(); + assert_eq!(res_list, vec![0, 0]); + + let res_list: Vec = g + .vertex(1) + .unwrap() + .after(1) + .edges() + .latest_time() + .flatten() + .collect(); + assert_eq!(res_list, vec![2, 2]); } #[test] @@ -1158,8 +1210,14 @@ mod db_tests { assert_eq!(g.vertex(1).unwrap().earliest_time(), Some(1)); assert_eq!(g.vertex(1).unwrap().latest_time(), Some(3)); - assert_eq!(g.at(2).vertex(1).unwrap().earliest_time(), Some(1)); + assert_eq!(g.at(2).vertex(1).unwrap().earliest_time(), Some(2)); assert_eq!(g.at(2).vertex(1).unwrap().latest_time(), Some(2)); + + assert_eq!(g.before(2).vertex(1).unwrap().earliest_time(), Some(1)); + assert_eq!(g.before(2).vertex(1).unwrap().latest_time(), Some(1)); + + assert_eq!(g.after(2).vertex(1).unwrap().earliest_time(), Some(3)); + assert_eq!(g.after(2).vertex(1).unwrap().latest_time(), Some(3)); } #[test] diff --git a/raphtory/src/db/graph/vertex.rs b/raphtory/src/db/graph/vertex.rs index 48151788e4..a50182c481 100644 --- a/raphtory/src/db/graph/vertex.rs +++ b/raphtory/src/db/graph/vertex.rs @@ -563,13 +563,25 @@ mod vertex_test { g.add_vertex(0, 1, NO_PROPS).unwrap(); g.add_vertex(1, 1, NO_PROPS).unwrap(); g.add_vertex(2, 1, NO_PROPS).unwrap(); - let mut view = g.at(1); + let view = g.before(2); assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 0); assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 1); - view = g.at(3); + let view = g.before(3); assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 0); assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 2); + + let view = g.after(0); + assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 1); + assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 2); + + let view = g.after(2); + assert_eq!(view.vertex(1), None); + assert_eq!(view.vertex(1), None); + + let view = g.at(1); + assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 1); + assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 1); } #[test] diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index 23f570ea39..a01d8a0e06 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -37,7 +37,6 @@ use std::{ #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GraphWithDeletions { graph: Arc, - partial_windows: bool, } impl Static for GraphWithDeletions {} @@ -46,7 +45,6 @@ impl From for GraphWithDeletions { fn from(value: InternalGraph) -> Self { Self { graph: Arc::new(value), - partial_windows: false, } } } @@ -64,110 +62,75 @@ impl Display for GraphWithDeletions { } impl GraphWithDeletions { - pub fn ignore_deletions_in_window(&self) -> GraphWithDeletions { - Self { - graph: self.graph.clone(), - partial_windows: true, - } - } - - pub fn include_deletions_in_window(&self) -> GraphWithDeletions { - Self { - graph: self.graph.clone(), - partial_windows: false, - } - } - - fn edge_alive_at(&self, e: &EdgeStore, range: Range, layer_ids: &LayerIds) -> bool { - let t = if self.partial_windows { - range.start - } else { - range.end - }; - - let get_last_addition = |v: &TimeIndex| { - if self.partial_windows { - v.range(i64::MIN..t.saturating_add(1)).last().copied() - } else { - v.range(i64::MIN..t).last().copied() - } + fn edge_alive_at(&self, e: &EdgeStore, t: i64, layer_ids: &LayerIds) -> bool { + let range = i64::MIN..t.saturating_add(1); + let ( + first_addition, + first_deletion, + last_addition_before_start, + last_deletion_before_start, + ) = match layer_ids { + LayerIds::None => return false, + LayerIds::All => ( + e.additions().iter().flat_map(|v| v.first()).min().copied(), + e.deletions().iter().flat_map(|v| v.first()).min().copied(), + e.additions() + .iter() + .flat_map(|v| v.range(range.clone()).last().copied()) + .max(), + e.deletions() + .iter() + .flat_map(|v| v.range(range.clone()).last().copied()) + .max(), + ), + LayerIds::One(l_id) => ( + e.additions().get(*l_id).and_then(|v| v.first().copied()), + e.deletions().get(*l_id).and_then(|v| v.first().copied()), + e.additions() + .get(*l_id) + .and_then(|v| v.range(range.clone()).last().copied()), + e.deletions() + .get(*l_id) + .and_then(|v| v.range(range.clone()).last().copied()), + ), + LayerIds::Multiple(ids) => ( + ids.iter() + .flat_map(|l_id| e.additions().get(*l_id).and_then(|v| v.first())) + .min() + .copied(), + ids.iter() + .flat_map(|l_id| e.deletions().get(*l_id).and_then(|v| v.first())) + .min() + .copied(), + ids.iter() + .flat_map(|l_id| { + e.additions() + .get(*l_id) + .and_then(|v| v.range(range.clone()).last().copied()) + }) + .max(), + ids.iter() + .flat_map(|l_id| { + e.deletions() + .get(*l_id) + .and_then(|v| v.range(range.clone()).last().copied()) + }) + .max(), + ), }; - let (first_addition, first_deletion, last_addition_before, last_deletion_before) = - match layer_ids { - LayerIds::None => return false, - LayerIds::All => ( - e.additions().iter().flat_map(|v| v.first()).min().copied(), - e.deletions().iter().flat_map(|v| v.first()).min().copied(), - e.additions() - .iter() - .flat_map(|v| get_last_addition(v)) - .max(), - e.deletions() - .iter() - .flat_map(|v| v.range(i64::MIN..t).last().copied()) - .max(), - ), - LayerIds::One(l_id) => ( - e.additions().get(*l_id).and_then(|v| v.first().copied()), - e.deletions().get(*l_id).and_then(|v| v.first().copied()), - e.additions().get(*l_id).and_then(|v| get_last_addition(v)), - e.deletions() - .get(*l_id) - .and_then(|v| v.range(i64::MIN..t).last().copied()), - ), - LayerIds::Multiple(ids) => ( - ids.iter() - .flat_map(|l_id| e.additions().get(*l_id).and_then(|v| v.first())) - .min() - .copied(), - ids.iter() - .flat_map(|l_id| e.deletions().get(*l_id).and_then(|v| v.first())) - .min() - .copied(), - ids.iter() - .flat_map(|l_id| { - e.additions().get(*l_id).and_then(|v| get_last_addition(v)) - }) - .max(), - ids.iter() - .flat_map(|l_id| { - e.deletions() - .get(*l_id) - .and_then(|v| v.range(i64::MIN..t).last().copied()) - }) - .max(), - ), - }; - - if self.partial_windows { - (first_deletion < first_addition - && first_deletion - .filter(|v| *v >= TimeIndexEntry::start(t)) - .is_some()) - || last_addition_before > last_deletion_before - } else { - (first_deletion < first_addition - && first_deletion - .filter(|v| *v >= TimeIndexEntry::end(t)) - .is_some()) - || match (last_addition_before, last_deletion_before) { - (Some(last_addition_before), Some(last_deletion_before)) => { - (last_addition_before.0 > last_deletion_before.0) - || (last_addition_before.0 == last_deletion_before.0 - && last_addition_before.0 == t - 1) //this is the case as we only want to include this entity if it is EXACTLY the time it was added/deleted - } - (Some(_), None) => true, - (None, Some(_)) => false, - (None, None) => false, - } - } + // None is less than any value (see test below) + (first_deletion < first_addition + && first_deletion + .filter(|v| *v >= TimeIndexEntry::start(t)) + .is_some()) + || last_addition_before_start > last_deletion_before_start } fn vertex_alive_at( &self, v: &VertexStore, - w: Range, + t: i64, layers: &LayerIds, edge_filter: Option<&EdgeFilter>, ) -> bool { @@ -176,7 +139,7 @@ impl GraphWithDeletions { .map(|eref| edges.get(eref.pid().into())) .find(|e| { edge_filter.map(|f| f(e, layers)).unwrap_or(true) - && self.edge_alive_at(e, w.clone(), layers) + && self.edge_alive_at(e, t, layers) }) .is_some() } @@ -184,7 +147,6 @@ impl GraphWithDeletions { pub fn new() -> Self { Self { graph: Arc::new(InternalGraph::default()), - partial_windows: false, } } @@ -251,7 +213,6 @@ impl InternalMaterialize for GraphWithDeletions { fn new_base_graph(&self, graph: InternalGraph) -> MaterializedGraph { MaterializedGraph::PersistentGraph(GraphWithDeletions { graph: Arc::new(graph), - partial_windows: false, }) } @@ -311,18 +272,14 @@ impl TimeSemantics for GraphWithDeletions { edge_filter: Option<&EdgeFilter>, ) -> bool { let v = self.graph.inner().storage.get_node(v); - v.active(w.clone()) || self.vertex_alive_at(&v, w, layer_ids, edge_filter) + v.active(w.clone()) || self.vertex_alive_at(&v, w.start, layer_ids, edge_filter) } fn include_edge_window(&self, e: &EdgeStore, w: Range, layer_ids: &LayerIds) -> bool { - if self.partial_windows { - //soft deletions - // includes edge if it is alive at the start of the window or added during the window - e.active(layer_ids, w.clone()) || self.edge_alive_at(e, w, layer_ids) - } else { - //includes edge if alive at the end of the window - self.edge_alive_at(e, w, layer_ids) - } + // includes edge if it is alive at the start of the window or added during the window + let active = e.active(layer_ids, w.clone()); + let alive = self.edge_alive_at(e, w.start, layer_ids); + e.active(layer_ids, w.clone()) || self.edge_alive_at(e, w.start, layer_ids) } fn vertex_history(&self, v: VID) -> Vec { @@ -335,7 +292,7 @@ impl TimeSemantics for GraphWithDeletions { fn edge_exploded(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { //Fixme: Need support for duration on exploded edges - if self.edge_alive_at(&self.core_edge(e.pid()), i64::MIN..i64::MIN, &layer_ids) { + if self.edge_alive_at(&self.core_edge(e.pid()), i64::MIN, &layer_ids) { Box::new( iter::once(e.at(i64::MIN.into())).chain(self.graph.edge_window_exploded( e, @@ -360,7 +317,7 @@ impl TimeSemantics for GraphWithDeletions { ) -> BoxedIter { // FIXME: Need better iterators on LockedView that capture the guard let entry = self.core_edge(e.pid()); - if self.edge_alive_at(&entry, w.clone(), &layer_ids) { + if self.edge_alive_at(&entry, w.start, &layer_ids) { Box::new( iter::once(e.at(w.start.into())).chain(self.graph.edge_window_exploded( e, @@ -397,7 +354,7 @@ impl TimeSemantics for GraphWithDeletions { fn edge_earliest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { e.time().map(|ti| *ti.t()).or_else(|| { let entry = self.core_edge(e.pid()); - if self.edge_alive_at(&entry, i64::MIN..i64::MIN, &layer_ids) { + if self.edge_alive_at(&entry, i64::MIN, &layer_ids) { Some(i64::MIN) } else { self.edge_additions(e, layer_ids).first().map(|ti| *ti.t()) @@ -412,7 +369,7 @@ impl TimeSemantics for GraphWithDeletions { layer_ids: LayerIds, ) -> Option { let entry = self.core_edge(e.pid()); - if self.edge_alive_at(&entry, w.clone(), &layer_ids) { + if self.edge_alive_at(&entry, w.start, &layer_ids) { Some(w.start) } else { self.edge_additions(e, layer_ids).range(w).first_t() @@ -433,7 +390,7 @@ impl TimeSemantics for GraphWithDeletions { )), None => { let entry = self.core_edge(e.pid()); - if self.edge_alive_at(&entry, i64::MIN..i64::MAX, &layer_ids) { + if self.edge_alive_at(&entry, i64::MAX, &layer_ids) { Some(i64::MAX) } else { self.edge_deletions(e, layer_ids).last_t() @@ -461,7 +418,7 @@ impl TimeSemantics for GraphWithDeletions { )), None => { let entry = self.core_edge(e.pid()); - if self.edge_alive_at(&entry, w.end - 1..w.end - 1, &layer_ids) { + if self.edge_alive_at(&entry, w.end - 1, &layer_ids) { Some(w.end - 1) } else { self.edge_deletions(e, layer_ids).range(w).last_t() @@ -582,7 +539,7 @@ impl TimeSemantics for GraphWithDeletions { match prop { Some(p) => { let entry = self.core_edge(e.pid()); - if self.edge_alive_at(&entry, start..end, &layer_ids) { + if self.edge_alive_at(&entry, start, &layer_ids) { p.last_before(start.saturating_add(1)) .into_iter() .map(|(_, v)| (start, v)) @@ -663,7 +620,7 @@ mod test_deletions { } #[test] - fn test_partial_vs_explicit_deletions() { + fn test_window_semantics() { let g = GraphWithDeletions::new(); g.add_edge(1, 1, 2, [("test", "test")], None).unwrap(); g.delete_edge(10, 1, 2, None).unwrap(); @@ -676,16 +633,10 @@ mod test_deletions { assert_eq!(g.at(9).count_edges(), 1); assert_eq!(g.window(5, 9).count_edges(), 1); assert_eq!(g.window(5, 10).count_edges(), 1); - assert_eq!(g.window(5, 11).count_edges(), 0); - - let g2 = g.ignore_deletions_in_window(); - assert_eq!(g2.at(12).count_edges(), 1); - assert_eq!(g2.at(11).count_edges(), 1); - assert_eq!(g2.at(10).count_edges(), 1); - assert_eq!(g2.at(9).count_edges(), 1); - assert_eq!(g2.window(5, 9).count_edges(), 1); - assert_eq!(g2.window(5, 10).count_edges(), 1); - assert_eq!(g2.window(5, 11).count_edges(), 1); + assert_eq!(g.window(5, 11).count_edges(), 1); + assert_eq!(g.window(10, 12).count_edges(), 0); + assert_eq!(g.before(10).count_edges(), 1); + assert_eq!(g.after(10).count_edges(), 0); } #[test] @@ -774,21 +725,11 @@ mod test_deletions { g.add_edge(2, 3, 4, [("test", "test")], None).unwrap(); g.delete_edge(2, 3, 4, None).unwrap(); - //for this I am assuming that deletions always happen after the creation - assert!(!g.window(0, 1).has_edge(1, 2, Layer::Default)); + assert!(g.window(0, 1).has_edge(1, 2, Layer::Default)); assert!(!g.window(0, 2).has_edge(3, 4, Layer::Default)); assert!(g.window(1, 2).has_edge(1, 2, Layer::Default)); - assert!(g.at(1).has_edge(1, 2, Layer::Default)); assert!(g.window(2, 3).has_edge(3, 4, Layer::Default)); assert!(!g.window(3, 4).has_edge(3, 4, Layer::Default)); - - let g2 = g.ignore_deletions_in_window(); - - assert!(g2.window(0, 1).has_edge(1, 2, Layer::Default)); - assert!(!g2.window(0, 2).has_edge(3, 4, Layer::Default)); - assert!(g2.window(1, 2).has_edge(1, 2, Layer::Default)); - assert!(g2.window(2, 3).has_edge(3, 4, Layer::Default)); - assert!(!g2.window(3, 4).has_edge(3, 4, Layer::Default)); } #[test] @@ -802,4 +743,24 @@ mod test_deletions { assert_eq!(e.window(0, 3).latest_time(), Some(2)); } + + #[test] + fn test_view_start_end() { + let g = GraphWithDeletions::new(); + let e = g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + assert_eq!(g.start(), Some(0)); + assert_eq!(g.end(), Some(1)); + e.delete(2, None).unwrap(); + assert_eq!(g.start(), Some(0)); + assert_eq!(g.end(), Some(3)); + let w = g.window(g.start().unwrap(), g.end().unwrap()); + assert!(g.has_edge(1, 2, Layer::All)); + assert!(w.has_edge(1, 2, Layer::All)); + assert_eq!(w.start(), Some(0)); + assert_eq!(w.end(), Some(3)); + + e.add_updates(4, NO_PROPS, None).unwrap(); + assert_eq!(g.start(), Some(0)); + assert_eq!(g.end(), Some(5)); + } } diff --git a/raphtory/src/graph_loader/mod.rs b/raphtory/src/graph_loader/mod.rs index 9516673954..7919f6af1a 100644 --- a/raphtory/src/graph_loader/mod.rs +++ b/raphtory/src/graph_loader/mod.rs @@ -186,14 +186,10 @@ mod graph_loader_test { let g_at_empty = g.at(1); let g_astart = g.at(7059); let g_at_another = g.at(28373); - let g_at_max = g.at(i64::MAX); - let g_at_min = g.at(i64::MIN); assert_eq!(g_at_empty.count_vertices(), 0); - assert_eq!(g_astart.count_vertices(), 70); - assert_eq!(g_at_another.count_vertices(), 123); - assert_eq!(g_at_max.count_vertices(), 139); - assert_eq!(g_at_min.count_vertices(), 0); + assert_eq!(g_astart.count_vertices(), 3); + assert_eq!(g_at_another.count_vertices(), 4); } #[test] diff --git a/raphtory/src/python/graph/edge.rs b/raphtory/src/python/graph/edge.rs index c6a7368fb9..fd8eec557e 100644 --- a/raphtory/src/python/graph/edge.rs +++ b/raphtory/src/python/graph/edge.rs @@ -6,7 +6,10 @@ //! use crate::{ core::{ - utils::{errors::GraphError, time::error::ParseTimeError}, + utils::{ + errors::GraphError, + time::{error::ParseTimeError, IntoTime}, + }, ArcStr, Direction, }, db::{ @@ -211,103 +214,6 @@ impl PyEdge { self.edge.dst().into() } - //****** Perspective APIS ******// - - /// Get the start time of the Edge. - /// - /// Returns: - /// The start time of the Edge. - #[getter] - pub fn start(&self) -> Option { - self.edge.start() - } - - /// Get the start datetime of the Edge. - /// - /// Returns: - /// The start datetime of the Edge. - #[getter] - pub fn start_date_time(&self) -> Option { - let start_time = self.edge.start()?; - NaiveDateTime::from_timestamp_millis(start_time) - } - - /// Get the end time of the Edge. - /// - /// Returns: - /// The end time of the Edge. - #[getter] - pub fn end(&self) -> Option { - self.edge.end() - } - - /// Get the end datetime of the Edge. - /// - /// Returns: - /// The end datetime of the Edge - #[getter] - pub fn end_date_time(&self) -> Option { - let end_time = self.edge.end()?; - NaiveDateTime::from_timestamp_millis(end_time) - } - - /// Get the duration of the Edge. - /// - /// Arguments: - /// step (int or str): The step size to use when calculating the duration. - /// - /// Returns: - /// A set of windows containing edges that fall in the time period - #[pyo3(signature = (step))] - fn expanding( - &self, - step: PyInterval, - ) -> Result>, ParseTimeError> { - self.edge.expanding(step) - } - - /// Get a set of Edge windows for a given window size, step, start time - /// and end time using rolling window. - /// A rolling window is a window that moves forward by `step` size at each iteration. - /// - /// Arguments: - /// window (int or str): The size of the window. - /// step (int or str): The step size to use when calculating the duration. (optional) - /// - /// Returns: - /// A set of windows containing edges that fall in the time period - fn rolling( - &self, - window: PyInterval, - step: Option, - ) -> Result>, ParseTimeError> { - self.edge.rolling(window, step) - } - - /// Get a new Edge with the properties of this Edge within the specified time window. - /// - /// Arguments: - /// start (int or str): The start time of the window (optional). - /// end (int or str): The end time of the window (optional). - /// - /// Returns: - /// A new Edge with the properties of this Edge within the specified time window. - #[pyo3(signature = (start = None, end = None))] - pub fn window( - &self, - start: Option, - end: Option, - ) -> EdgeView> { - self.edge - .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) - } - /// Get a new Edge with the properties of this Edge within the specified layer. - /// - /// Arguments: - /// layer_names (str): Layer to be included in the new edge. - /// - /// Returns: - /// A new Edge with the properties of this Edge within the specified time window. #[pyo3(signature = (name))] pub fn layer(&self, name: String) -> PyResult>> { if let Some(edge) = self.edge.layer(name.clone()) { @@ -344,18 +250,6 @@ impl PyEdge { } } - /// Get a new Edge with the properties of this Edge at a specified time. - /// - /// Arguments: - /// end (int, str or datetime(utrc)): The time to get the properties at. - /// - /// Returns: - /// A new Edge with the properties of this Edge at a specified time. - #[pyo3(signature = (end))] - pub fn at(&self, end: PyTime) -> EdgeView> { - self.edge.at(end) - } - /// Explodes an Edge into a list of PyEdges. This is useful when you want to iterate over /// the properties of an Edge at every single point in time. This will return a seperate edge /// each time a property had been changed. @@ -457,6 +351,8 @@ impl PyEdge { } } +impl_timeops!(PyEdge, edge, EdgeView, "edge"); + impl Repr for PyEdge { fn repr(&self) -> String { self.edge.repr() @@ -850,6 +746,52 @@ impl PyEdges { .into() } + /// Filter edges to only include events before `end`. + /// + /// Arguments: + /// end(int): The end time of the window (exclusive). + /// + /// Returns: + /// A list of edges with the window filter applied. + fn before(&self, end: PyTime) -> PyEdges { + let builder = self.builder.clone(); + let end = end.into_time(); + + (move || { + let box_builder: Box<(dyn Iterator> + Send + 'static)> = + Box::new( + builder() + .map(move |e| e.before(end)) + .map(|e| >::from(e)), + ); + box_builder + }) + .into() + } + + /// Filter edges to only include events after `start`. + /// + /// Arguments: + /// start(int): The start time of the window (exclusive). + /// + /// Returns: + /// A list of edges with the window filter applied. + fn after(&self, start: PyTime) -> PyEdges { + let builder = self.builder.clone(); + let start = start.into_time(); + + (move || { + let box_builder: Box<(dyn Iterator> + Send + 'static)> = + Box::new( + builder() + .map(move |e| e.after(start)) + .map(|e| >::from(e)), + ); + box_builder + }) + .into() + } + fn __repr__(&self) -> String { self.repr() } diff --git a/raphtory/src/python/graph/graph_with_deletions.rs b/raphtory/src/python/graph/graph_with_deletions.rs index eb830c392c..fbf9e85c5a 100644 --- a/raphtory/src/python/graph/graph_with_deletions.rs +++ b/raphtory/src/python/graph/graph_with_deletions.rs @@ -88,13 +88,6 @@ impl PyGraphWithDeletions { ) } - pub fn include_deletions_in_window(&self, py: Python) -> PyObject { - self.graph.include_deletions_in_window().into_py(py) - } - - pub fn ignore_deletions_in_window(&self, py: Python) -> PyObject { - self.graph.ignore_deletions_in_window().into_py(py) - } /// Adds a new vertex with the given id and properties to the graph. /// /// Arguments: diff --git a/raphtory/src/python/graph/vertex.rs b/raphtory/src/python/graph/vertex.rs index 8aabde0857..f4fdc25677 100644 --- a/raphtory/src/python/graph/vertex.rs +++ b/raphtory/src/python/graph/vertex.rs @@ -262,116 +262,6 @@ impl PyVertex { self.vertex.out_neighbours().into() } - //****** Perspective APIS ******// - - /// Gets the earliest time that this vertex is valid. - /// - /// Returns: - /// The earliest time that this vertex is valid or None if the vertex is valid for all times. - #[getter] - pub fn start(&self) -> Option { - self.vertex.start() - } - - /// Gets the earliest datetime that this vertex is valid - /// - /// Returns: - /// The earliest datetime that this vertex is valid or None if the vertex is valid for all times. - #[getter] - pub fn start_date_time(&self) -> Option { - let start_time = self.vertex.start()?; - NaiveDateTime::from_timestamp_millis(start_time) - } - - /// Gets the latest time that this vertex is valid. - /// - /// Returns: - /// The latest time that this vertex is valid or None if the vertex is valid for all times. - #[getter] - pub fn end(&self) -> Option { - self.vertex.end() - } - - /// Gets the latest datetime that this vertex is valid - /// - /// Returns: - /// The latest datetime that this vertex is valid or None if the vertex is valid for all times. - #[getter] - pub fn end_date_time(&self) -> Option { - let end_time = self.vertex.end()?; - NaiveDateTime::from_timestamp_millis(end_time) - } - - /// Creates a `PyVertexWindowSet` with the given `step` size and optional `start` and `end` times, - /// using an expanding window. - /// - /// An expanding window is a window that grows by `step` size at each iteration. - /// This will tell you whether a vertex exists at different points in the window and what - /// its properties are at those points. - /// - /// Arguments: - /// step (int): The step size of the window. - /// - /// Returns: - /// A `PyVertexWindowSet` object. - fn expanding( - &self, - step: PyInterval, - ) -> Result>, ParseTimeError> { - self.vertex.expanding(step) - } - - /// Creates a `PyVertexWindowSet` with the given `window` size and optional `step`, `start` and `end` times, - /// using a rolling window. - /// - /// A rolling window is a window that moves forward by `step` size at each iteration. - /// This will tell you whether a vertex exists at different points in the window and what - /// its properties are at those points. - /// - /// Arguments: - /// window: The size of the window. - /// step: The step size of the window. Defaults to the window size. - /// - /// Returns: - /// A `PyVertexWindowSet` object. - fn rolling( - &self, - window: PyInterval, - step: Option, - ) -> Result>, ParseTimeError> { - self.vertex.rolling(window, step) - } - - /// Create a view of the vertex including all events between `start` (inclusive) and `end` (exclusive) - /// - /// Arguments: - /// start (int, str or datetime(utc)): The start time of the window. Defaults to the start time of the vertex. - /// end (int, str or datetime(utc)): The end time of the window. Defaults to the end time of the vertex. - /// - /// Returns: - /// A `PyVertex` object. - #[pyo3(signature = (start = None, end = None))] - pub fn window( - &self, - start: Option, - end: Option, - ) -> VertexView> { - self.vertex - .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) - } - - /// Create a view of the vertex including all events at `t`. - /// - /// Arguments: - /// end (int, str or datetime(utc)): The time of the window. - /// - /// Returns: - /// A `PyVertex` object. - #[pyo3(signature = (end))] - pub fn at(&self, end: PyTime) -> VertexView> { - self.vertex.at(end) - } - #[doc = default_layer_doc_string!()] pub fn default_layer(&self) -> PyVertex { self.vertex.default_layer().into() @@ -410,6 +300,9 @@ impl PyVertex { self.repr() } } + +impl_timeops!(PyVertex, vertex, VertexView, "vertex"); + impl Repr for PyVertex { fn repr(&self) -> String { self.vertex.repr() @@ -681,95 +574,6 @@ impl PyVertices { fn collect(&self) -> Vec { self.__iter__().into_iter().collect() } - - //***** Perspective APIS ******// - /// Returns the start time of the vertices - #[getter] - pub fn start(&self) -> Option { - self.vertices.start() - } - - /// Returns the end time of the vertices - #[getter] - pub fn end(&self) -> Option { - self.vertices.end() - } - - #[doc = window_size_doc_string!()] - #[getter] - pub fn window_size(&self) -> Option { - self.vertices.window_size() - } - - /// Creates a PyVertexWindowSet with the given step size using an expanding window. - /// - /// An expanding window is a window that grows by step size at each iteration. - /// This will tell you whether a vertex exists at different points in the window - /// and what its properties are at those points. - /// - /// Arguments: - /// `step` - The step size of the window - /// - /// Returns: - /// A PyVertexWindowSet with the given step size and optional start and end times or an error - fn expanding( - &self, - step: PyInterval, - ) -> Result>, ParseTimeError> { - self.vertices.expanding(step) - } - - /// Creates a PyVertexWindowSet with the given window size and optional step using a rolling window. - /// - /// A rolling window is a window that moves forward by step size at each iteration. - /// This will tell you whether a vertex exists at different points in the window and - /// what its properties are at those points. - /// - /// Arguments: - /// window (str or int): The window size of the window - /// step (str or int): The step size of the window - /// - /// Returns: - /// A PyVertexWindowSet with the given window size and optional step size or an error - fn rolling( - &self, - window: PyInterval, - step: Option, - ) -> Result>, ParseTimeError> { - self.vertices.rolling(window, step) - } - - /// Create a view of the vertices including all events between start (inclusive) and - /// end (exclusive) - /// - /// Arguments: - /// start (int, str or datetime(utc)): The start time of the window (inclusive) - /// end (int, str or datetime(utc)): The end time of the window (exclusive) - /// - /// Returns: - /// A `PyVertices` object of vertices within the window. - #[pyo3(signature = (start = None, end = None))] - pub fn window( - &self, - start: Option, - end: Option, - ) -> Vertices> { - self.vertices - .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) - } - - /// Create a view of the vertices including all events at `t`. - /// - /// Arguments: - /// end (int, str or datetime(utc)): The time of the window. - /// - /// Returns: - /// A `PyVertices` object. - #[pyo3(signature = (end))] - pub fn at(&self, end: PyTime) -> Vertices> { - self.vertices.at(end) - } - #[doc = default_layer_doc_string!()] pub fn default_layer(&self) -> PyVertices { self.vertices.default_layer().into() @@ -805,6 +609,8 @@ impl PyVertices { } } +impl_timeops!(PyVertices, vertices, Vertices, "vertices"); + impl Repr for PyVertices { fn repr(&self) -> String { format!("Vertices({})", iterator_repr(self.__iter__().into_iter())) @@ -903,60 +709,6 @@ impl PyPathFromGraph { self.path.neighbours().into() } - //****** Perspective APIS ******// - #[getter] - pub fn start(&self) -> Option { - self.path.start() - } - - #[getter] - pub fn end(&self) -> Option { - self.path.end() - } - - #[doc = window_size_doc_string!()] - #[getter] - pub fn window_size(&self) -> Option { - self.path.window_size() - } - - fn expanding( - &self, - step: PyInterval, - ) -> Result>, ParseTimeError> { - self.path.expanding(step) - } - - fn rolling( - &self, - window: PyInterval, - step: Option, - ) -> Result>, ParseTimeError> { - self.path.rolling(window, step) - } - - #[pyo3(signature = (start = None, end = None))] - pub fn window( - &self, - start: Option, - end: Option, - ) -> PathFromGraph> { - self.path - .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) - } - - /// Create a view of the vertex including all events at `t`. - /// - /// Arguments: - /// end (int): The time of the window. - /// - /// Returns: - /// A `PyVertex` object. - #[pyo3(signature = (end))] - pub fn at(&self, end: PyTime) -> PathFromGraph> { - self.path.at(end) - } - #[doc = default_layer_doc_string!()] pub fn default_layer(&self) -> Self { self.path.default_layer().into() @@ -973,6 +725,8 @@ impl PyPathFromGraph { } } +impl_timeops!(PyPathFromGraph, path, PathFromGraph, "path"); + impl Repr for PyPathFromGraph { fn repr(&self) -> String { format!( @@ -1110,60 +864,6 @@ impl PyPathFromVertex { self.path.neighbours().into() } - //****** Perspective APIS ******// - #[getter] - pub fn start(&self) -> Option { - self.path.start() - } - - #[getter] - pub fn end(&self) -> Option { - self.path.end() - } - - #[doc = window_size_doc_string!()] - #[getter] - pub fn window_size(&self) -> Option { - self.path.window_size() - } - - fn expanding( - &self, - step: PyInterval, - ) -> Result>, ParseTimeError> { - self.path.expanding(step) - } - - fn rolling( - &self, - window: PyInterval, - step: Option, - ) -> Result>, ParseTimeError> { - self.path.rolling(window, step) - } - - #[pyo3(signature = (start = None,end = None))] - pub fn window( - &self, - start: Option, - end: Option, - ) -> PathFromVertex> { - self.path - .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) - } - - /// Create a view of the vertex including all events at `t`. - /// - /// Arguments: - /// end (int): The time of the window. - /// - /// Returns: - /// A `PyVertex` object. - #[pyo3(signature = (end))] - pub fn at(&self, end: PyTime) -> PathFromVertex> { - self.path.at(end) - } - pub fn default_layer(&self) -> Self { self.path.default_layer().into() } @@ -1179,6 +879,8 @@ impl PyPathFromVertex { } } +impl_timeops!(PyPathFromVertex, path, PathFromVertex, "path"); + impl Repr for PyPathFromVertex { fn repr(&self) -> String { format!( diff --git a/raphtory/src/python/graph/views/graph_view.rs b/raphtory/src/python/graph/views/graph_view.rs index d6a6010892..859271c6b0 100644 --- a/raphtory/src/python/graph/views/graph_view.rs +++ b/raphtory/src/python/graph/views/graph_view.rs @@ -243,119 +243,6 @@ impl PyGraphView { (move || clone.edges()).into() } - //****** Perspective APIS ******// - - /// Returns the default start time for perspectives over the view - /// - /// Returns: - /// the default start time for perspectives over the view - #[getter] - pub fn start(&self) -> Option { - self.graph.start() - } - - /// Returns the default start datetime for perspectives over the view - /// - /// Returns: - /// the default start datetime for perspectives over the view - #[getter] - pub fn start_date_time(&self) -> Option { - let start_time = self.graph.start()?; - NaiveDateTime::from_timestamp_millis(start_time) - } - - /// Returns the default end time for perspectives over the view - /// - /// Returns: - /// the default end time for perspectives over the view - #[getter] - pub fn end(&self) -> Option { - self.graph.end() - } - - #[doc = window_size_doc_string!()] - pub fn window_size(&self) -> Option { - self.graph.window_size() - } - - /// Returns the default end datetime for perspectives over the view - /// - /// Returns: - /// the default end datetime for perspectives over the view - #[getter] - pub fn end_date_time(&self) -> Option { - let end_time = self.graph.end()?; - NaiveDateTime::from_timestamp_millis(end_time) - } - - /// Creates a `WindowSet` with the given `step` size and optional `start` and `end` times, - /// using an expanding window. - /// - /// An expanding window is a window that grows by `step` size at each iteration. - /// - /// Arguments: - /// step (int) : the size of the window - /// start (int): the start time of the window (optional) - /// end (int): the end time of the window (optional) - /// - /// Returns: - /// A `WindowSet` with the given `step` size and optional `start` and `end` times, - #[pyo3(signature = (step))] - fn expanding(&self, step: PyInterval) -> Result, ParseTimeError> { - self.graph.expanding(step) - } - - /// Creates a `WindowSet` with the given `window` size and optional `step`, `start` and `end` times, - /// using a rolling window. - /// - /// A rolling window is a window that moves forward by `step` size at each iteration. - /// - /// Arguments: - /// window (int): the size of the window - /// step (int): the size of the step (optional) - /// start (int): the start time of the window (optional) - /// end: the end time of the window (optional) - /// - /// Returns: - /// a `WindowSet` with the given `window` size and optional `step`, `start` and `end` times, - fn rolling( - &self, - window: PyInterval, - step: Option, - ) -> Result, ParseTimeError> { - self.graph.rolling(window, step) - } - - /// Create a view including all events between `start` (inclusive) and `end` (exclusive) - /// - /// Arguments: - /// start (int): the start time of the window (optional) - /// end (int): the end time of the window (optional) - /// - /// Returns: - /// a view including all events between `start` (inclusive) and `end` (exclusive) - #[pyo3(signature = (start=None, end=None))] - pub fn window( - &self, - start: Option, - end: Option, - ) -> WindowedGraph { - self.graph - .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) - } - - /// Create a view including all events until `end` (inclusive) - /// - /// Arguments: - /// end (int) : the end time of the window - /// - /// Returns: - /// a view including all events until `end` (inclusive) - #[pyo3(signature = (end))] - pub fn at(&self, end: PyTime) -> WindowedGraph { - self.graph.at(end) - } - #[doc = default_layer_doc_string!()] pub fn default_layer(&self) -> LayeredGraph { self.graph.default_layer() @@ -416,6 +303,8 @@ impl PyGraphView { } } +impl_timeops!(PyGraphView, graph, DynamicGraph, "graph"); + impl Repr for PyGraphView { fn repr(&self) -> String { let num_edges = self.graph.count_edges(); diff --git a/raphtory/src/python/types/macros/mod.rs b/raphtory/src/python/types/macros/mod.rs index 8d589e93db..c230fa1045 100644 --- a/raphtory/src/python/types/macros/mod.rs +++ b/raphtory/src/python/types/macros/mod.rs @@ -4,3 +4,6 @@ pub mod iterable; pub mod nested_iterable; #[macro_use] pub mod cmp; + +#[macro_use] +pub mod timeops; diff --git a/raphtory/src/python/types/macros/timeops.rs b/raphtory/src/python/types/macros/timeops.rs new file mode 100644 index 0000000000..52f329e319 --- /dev/null +++ b/raphtory/src/python/types/macros/timeops.rs @@ -0,0 +1,140 @@ +/// Macro for implementing all the timeops methods on a python wrapper +/// +/// # Arguments +/// * obj: The struct the methods should be implemented for +/// * field: The name of the struct field holding the rust struct implementing `TimeOps` +/// * base_type: The rust type of `field` (note that `<$base_type as TimeOps>::WindowedViewType` +/// and `WindowSet<$base_type>` should have an `IntoPy` implementation) +/// * name: The name of the object that appears in the docstring +macro_rules! impl_timeops { + ($obj:ty, $field:ident, $base_type:ty, $name:literal) => { + #[pyo3::pymethods] + impl $obj { + #[doc = concat!(r" Gets the start time for rolling and expanding windows for this ", $name)] + /// + /// Returns: + #[doc = concat!(r" The earliest time that this ", $name, r" is valid or None if the ", $name, r" is valid for all times.")] + #[getter] + pub fn start(&self) -> Option { + self.$field.start() + } + + #[doc = concat!(r" Gets the earliest datetime that this ", $name, r" is valid")] + /// + /// Returns: + #[doc = concat!(r" The earliest datetime that this ", $name, r" is valid or None if the ", $name, r" is valid for all times.")] + #[getter] + pub fn start_date_time(&self) -> Option { + let start_time = self.$field.start()?; + NaiveDateTime::from_timestamp_millis(start_time) + } + + #[doc = concat!(r" Gets the latest time that this ", $name, r" is valid.")] + /// + /// Returns: + #[doc = concat!(" The latest time that this ", $name, r" is valid or None if the ", $name, r" is valid for all times.")] + #[getter] + pub fn end(&self) -> Option { + self.$field.end() + } + + #[doc = concat!(r" Gets the latest datetime that this ", $name, r" is valid")] + /// + /// Returns: + #[doc = concat!(r" The latest datetime that this ", $name, r" is valid or None if the ", $name, r" is valid for all times.")] + #[getter] + pub fn end_date_time(&self) -> Option { + let end_time = self.$field.end()?; + NaiveDateTime::from_timestamp_millis(end_time) + } + + #[doc = concat!(r" Get the window size (difference between start and end) for this ", $name)] + #[getter] + pub fn window_size(&self) -> Option { + self.$field.window_size() + } + + /// Creates a `WindowSet` with the given `step` size using an expanding window. + /// + /// An expanding window is a window that grows by `step` size at each iteration. + /// + /// Arguments: + /// step (int): The step size of the window. + /// + /// Returns: + /// A `WindowSet` object. + fn expanding(&self, step: PyInterval) -> Result, ParseTimeError> { + self.$field.expanding(step) + } + + /// Creates a `WindowSet` with the given `window` size and optional `step` using a rolling window. + /// + /// A rolling window is a window that moves forward by `step` size at each iteration. + /// + /// Arguments: + /// window: The size of the window. + /// step: The step size of the window. Defaults to the window size. + /// + /// Returns: + /// A `WindowSet` object. + fn rolling( + &self, + window: PyInterval, + step: Option, + ) -> Result, ParseTimeError> { + self.$field.rolling(window, step) + } + + #[doc = concat!(r" Create a view of the ", $name, r" including all events between `start` (inclusive) and `end` (exclusive)")] + /// + /// Arguments: + #[doc = concat!(r" start: The start time of the window. Defaults to the start time of the ", $name, r".")] + #[doc = concat!(r" end: The end time of the window. Defaults to the end time of the ", $name, r".")] + /// + /// Returns: + #[doc = concat!("r A ", $name, " object.")] + #[pyo3(signature = (start = None, end = None))] + pub fn window( + &self, + start: Option, + end: Option, + ) -> <$base_type as TimeOps>::WindowedViewType { + self.$field + .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) + } + + #[doc = concat!(r" Create a view of the ", $name, r" including all events at `time`.")] + /// + /// Arguments: + /// time: The time of the window. + /// + /// Returns: + #[doc = concat!(r" A ", $name, r" object.")] + pub fn at(&self, time: PyTime) -> <$base_type as TimeOps>::WindowedViewType { + self.$field.at(time) + } + + #[doc = concat!(r" Create a view of the ", $name, r" including all events before `end` (exclusive).")] + /// + /// Arguments: + /// end: The end time of the window. + /// + /// Returns: + #[doc = concat!(r" A ", $name, r" object.")] + pub fn before(&self, end: PyTime) -> <$base_type as TimeOps>::WindowedViewType { + self.$field.before(end) + } + + #[doc = concat!(r" Create a view of the ", $name, r" including all events after `start` (exclusive).")] + /// + /// Arguments: + /// start: The start time of the window. + /// + /// Returns: + #[doc = concat!(r" A ", $name, r" object.")] + pub fn after(&self, start: PyTime) -> <$base_type as TimeOps>::WindowedViewType { + self.$field.after(start) + } + } + }; +}