Skip to content

Commit

Permalink
Added timezoned datetime support to python (#1443)
Browse files Browse the repository at this point in the history
* fixing bits

* Added timezone support in python

* fmt

* much cleaner

* put naive conversion back

* fixed warnings

---------

Co-authored-by: Lucas Jeub <[email protected]>
  • Loading branch information
miratepuffin and ljeub-pometry authored Jan 8, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 2a96a2e commit f9a583c
Showing 7 changed files with 100 additions and 52 deletions.
1 change: 1 addition & 0 deletions python/python/raphtory/__init__.py
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@

try:
from importlib.metadata import version as _version

__version__ = _version(__name__)
except Exception:
# either 3.7 or package not installed, just don't set a version
90 changes: 51 additions & 39 deletions python/tests/notebook.ipynb
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@
],
"source": [
"from raphtory import Graph\n",
"\n",
"g = Graph()\n",
"g"
]
@@ -73,7 +74,7 @@
"g.add_edge(0, 1, 3, layer=\"layer1\")\n",
"g.add_edge(0, 1, 4, layer=\"layer2\")\n",
"\n",
"g.nodes.edges.layer_names\n"
"g.nodes.edges.layer_names"
]
},
{
@@ -119,37 +120,47 @@
],
"source": [
"# Basic Addition with integer IDs\n",
"g.add_node(timestamp=1,id=10)\n",
"g.add_edge(timestamp=2,src=1,dst=2)\n",
"g.add_node(timestamp=1, id=10)\n",
"g.add_edge(timestamp=2, src=1, dst=2)\n",
"\n",
"# checking node 10, 1 and 5 exist \n",
"# checking node 10, 1 and 5 exist\n",
"print(g.has_node(10), g.has_node(1), g.has_node(5))\n",
"# checking edge 1,2 exists and 2,1 doesn't as Raphtory is directed\n",
"print(g.has_edge(1,2),g.has_edge(2,1))\n",
"print(g.has_edge(1, 2), g.has_edge(2, 1))\n",
"# Check the total number of edges and nodes\n",
"print(g.count_edges(),g.count_nodes())\n",
"print(g.count_edges(), g.count_nodes())\n",
"\n",
"# Adding nodes and edges with String IDs\n",
"g.add_node(timestamp=5,id=\"Ben\")\n",
"g.add_edge(timestamp=8,src=\"Hamza\",dst=\"Ben\", layer=\"toad\")\n",
"g.add_node(timestamp=5, id=\"Ben\")\n",
"g.add_edge(timestamp=8, src=\"Hamza\", dst=\"Ben\", layer=\"toad\")\n",
"\n",
"# Performing the same checks as before, but with strings\n",
"print(g.has_node(id=\"Ben\"), g.has_node(id=\"Hamza\"), g.has_node(id=\"Dave\"))\n",
"print(g.has_edge(src=\"Hamza\",dst=\"Ben\"),g.has_edge(src=\"Ben\",dst=\"Hamza\"))\n",
"print(g.count_edges(),g.count_nodes())\n",
"print(g.has_edge(src=\"Hamza\", dst=\"Ben\"), g.has_edge(src=\"Ben\", dst=\"Hamza\"))\n",
"print(g.count_edges(), g.count_nodes())\n",
"\n",
"g.add_edge(0, 1, 3, layer=\"toad\")\n",
"#Add an edge with Temporal Properties which can change over time\n",
"e = g.add_edge(timestamp=7,src=\"Haaroon\",dst=\"Hamza\",properties={\"property1\": 1, \"property2\": 9.8, \"property3\": \"test\"}, layer=\"toad\")\n",
"#Add a static property which is immutable\n",
"e.add_constant_properties(properties={\"First-Met\":\"01/01/1990\"})\n",
"# Add an edge with Temporal Properties which can change over time\n",
"e = g.add_edge(\n",
" timestamp=7,\n",
" src=\"Haaroon\",\n",
" dst=\"Hamza\",\n",
" properties={\"property1\": 1, \"property2\": 9.8, \"property3\": \"test\"},\n",
" layer=\"toad\",\n",
")\n",
"# Add a static property which is immutable\n",
"e.add_constant_properties(properties={\"First-Met\": \"01/01/1990\"})\n",
"\n",
"#Add an node with Temporal Properties which can change over time\n",
"v = g.add_node(timestamp=5,id=\"Hamza\",properties= {\"property1\": 5, \"property2\": 12.5, \"property3\": \"test2\"})\n",
"#Add a static property which is immutable\n",
"v.add_constant_properties(properties={\"Date-of-Birth\":\"01/01/1990\"})\n",
"# Add an node with Temporal Properties which can change over time\n",
"v = g.add_node(\n",
" timestamp=5,\n",
" id=\"Hamza\",\n",
" properties={\"property1\": 5, \"property2\": 12.5, \"property3\": \"test2\"},\n",
")\n",
"# Add a static property which is immutable\n",
"v.add_constant_properties(properties={\"Date-of-Birth\": \"01/01/1990\"})\n",
"print(g.node(\"Ben\").__repr__())\n",
"print(g.edge(\"Haaroon\",\"Hamza\").__repr__())\n",
"print(g.edge(\"Haaroon\", \"Hamza\").__repr__())\n",
"print(g.__repr__())\n",
"with tempfile.NamedTemporaryFile() as g_path:\n",
" g.save_to_file(g_path.name)\n",
@@ -220,9 +231,9 @@
"\n",
"g = graph_loader.lotr_graph()\n",
"view = g.at(300)\n",
"g.add_node(timestamp=0,id=\"Gandalf\",properties={\"Race\":\"Maiar\"})\n",
"g.add_node(timestamp=0, id=\"Gandalf\", properties={\"Race\": \"Maiar\"})\n",
"\n",
"#view[\"Gandalf\"][\"Race\"]\n",
"# view[\"Gandalf\"][\"Race\"]\n",
"from raphtory import export\n",
"\n",
"export.to_networkx(view)"
@@ -258,16 +269,16 @@
"from raphtory import graph_gen\n",
"\n",
"g = Graph()\n",
"graph_gen.ba_preferential_attachment(g,nodes_to_add=1000,edges_per_step=10)\n",
"view = g.window(0,1000)\n",
"graph_gen.ba_preferential_attachment(g, nodes_to_add=1000, edges_per_step=10)\n",
"view = g.window(0, 1000)\n",
"\n",
"ids = []\n",
"degrees = []\n",
"for v in view.nodes:\n",
" ids.append(v.id)\n",
" degrees.append(v.degree())\n",
"\n",
"df = pd.DataFrame.from_dict({\"id\":ids,\"degree\": degrees})\n",
"df = pd.DataFrame.from_dict({\"id\": ids, \"degree\": degrees})\n",
"\n",
"sns.set()\n",
"sns.histplot(df.degree)"
@@ -286,6 +297,7 @@
"from raphtory import Graph\n",
"from raphtory import algorithms\n",
"from raphtory import graph_loader\n",
"\n",
"g = graph_loader.lotr_graph()\n",
"views_l1 = g.rolling(1000)"
]
@@ -319,23 +331,23 @@
"source": [
"views = g.expanding(100)\n",
"\n",
"timestamps = []\n",
"timestamps = []\n",
"node_count = []\n",
"edge_count = []\n",
"degree = []\n",
"edge_count = []\n",
"degree = []\n",
"\n",
"for view in views:\n",
" timestamps.append(view.latest_time)\n",
" #node_count.append(view.num_nodes()) \n",
" #edge_count.append(view.num_edges())\n",
" degree.append(view.count_edges()/max(1,view.count_nodes())) \n",
" \n",
" # node_count.append(view.num_nodes())\n",
" # edge_count.append(view.num_edges())\n",
" degree.append(view.count_edges() / max(1, view.count_nodes()))\n",
"\n",
"sns.set_context()\n",
"ax = plt.gca()\n",
"plt.xticks(rotation=45)\n",
"ax.set_xlabel(\"Time\")\n",
"ax.set_ylabel(\"Average Interactions\")\n",
"sns.lineplot(x = timestamps, y = degree,ax=ax) "
"sns.lineplot(x=timestamps, y=degree, ax=ax)"
]
},
{
@@ -365,26 +377,26 @@
}
],
"source": [
"views = g.expanding(step=10) \n",
"views = g.expanding(step=10)\n",
"\n",
"timestamps = []\n",
"degree = []\n",
"timestamps = []\n",
"degree = []\n",
"\n",
"for view in views:\n",
" timestamps.append(view.latest_time)\n",
" gandalf = view.node(\"Gandalf\")\n",
" if(gandalf is not None):\n",
" if gandalf is not None:\n",
" degree.append(gandalf.degree())\n",
" else:\n",
" degree.append(0)\n",
" \n",
" \n",
"\n",
"\n",
"sns.set_context()\n",
"ax = plt.gca()\n",
"plt.xticks(rotation=45)\n",
"ax.set_xlabel(\"Time\")\n",
"ax.set_ylabel(\"Interactions\")\n",
"sns.lineplot(x = timestamps, y = degree,ax=ax) "
"sns.lineplot(x=timestamps, y=degree, ax=ax)"
]
}
],
32 changes: 32 additions & 0 deletions python/tests/test_graphdb.py
Original file line number Diff line number Diff line change
@@ -1524,6 +1524,38 @@ def test_datetime_add_node():
assert view.node(2).latest_date_time == datetime.datetime(2014, 2, 3, 0, 0)


def test_datetime_with_timezone():
from datetime import datetime
from raphtory import Graph
import pytz

g = Graph()
# testing zones east and west of UK
timezones = [
"Asia/Kolkata",
"America/New_York",
"US/Central",
"Europe/London",
"Australia/Sydney",
"Africa/Johannesburg",
]
results = [
datetime(2024, 1, 5, 1, 0),
datetime(2024, 1, 5, 6, 30),
datetime(2024, 1, 5, 10, 0),
datetime(2024, 1, 5, 12, 0),
datetime(2024, 1, 5, 17, 0),
datetime(2024, 1, 5, 18, 0),
]

for tz in timezones:
timezone = pytz.timezone(tz)
naive_datetime = datetime(2024, 1, 5, 12, 0, 0)
localized_datetime = timezone.localize(naive_datetime)
g.add_node(localized_datetime, 1)
assert g.node(1).history_date_time() == results


def test_equivalent_nodes_edges_and_sets():
g = Graph()
g.add_node(1, 1)
3 changes: 1 addition & 2 deletions raphtory/src/algorithms/community_detection/louvain.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use crate::{
algorithms::{
algorithm_result::AlgorithmResult,
community_detection::modularity::{ComID, ModularityFunction, Partition},
community_detection::modularity::{ModularityFunction, Partition},
},
core::entities::VID,
prelude::GraphViewOps,
};
use itertools::Itertools;
use rand::prelude::SliceRandom;
use std::collections::HashMap;

4 changes: 2 additions & 2 deletions raphtory/src/algorithms/community_detection/modularity.rs
Original file line number Diff line number Diff line change
@@ -183,7 +183,7 @@ impl ModularityFunction for ModularityUnDir {
partition: Partition,
tol: f64,
) -> Self {
let n = graph.count_nodes();
let _n = graph.count_nodes();
let local_id_map: HashMap<_, _> = graph
.nodes()
.iter()
@@ -352,7 +352,7 @@ impl ModularityFunction for ModularityUnDir {
let (new_partition, new_to_old, old_to_new) = old_partition.compact();
let adj_com: Vec<_> = new_partition
.coms()
.map(|(c_new, com)| {
.map(|(_c_new, com)| {
let mut neighbours = HashMap::new();
for n in com {
for (c_old, w) in &self.adj_com[n.index()] {
12 changes: 6 additions & 6 deletions raphtory/src/algorithms/layout/fruchterman_reingold.rs
Original file line number Diff line number Diff line change
@@ -67,8 +67,8 @@ pub fn fruchterman_reingold<'graph, G: GraphViewOps<'graph>>(
let repulsive_f = repulsive_force(repulsion, k, distance);
// Modify the first value of the array for key 1
if let Some(arr) = node_disp.get_mut(&node_v_id) {
arr[0] += (delta.get(0).unwrap() / distance * repulsive_f);
arr[1] += (delta.get(1).unwrap() / distance * repulsive_f);
arr[0] += delta.get(0).unwrap() / distance * repulsive_f;
arr[1] += delta.get(1).unwrap() / distance * repulsive_f;
}
}
}
@@ -89,12 +89,12 @@ pub fn fruchterman_reingold<'graph, G: GraphViewOps<'graph>>(
let distance = calculate_distance(&delta);
let attractive_f = attractive_force(attraction, k, distance);
if let Some(arr) = node_disp.get_mut(&node_v_id) {
arr[0] -= (delta.get(0).unwrap() / distance * attractive_f);
arr[1] -= (delta.get(1).unwrap() / distance * attractive_f);
arr[0] -= delta.get(0).unwrap() / distance * attractive_f;
arr[1] -= delta.get(1).unwrap() / distance * attractive_f;
}
if let Some(arr) = node_disp.get_mut(&node_u_id) {
arr[0] += (delta.get(0).unwrap() / distance * attractive_f);
arr[1] += (delta.get(1).unwrap() / distance * attractive_f);
arr[0] += delta.get(0).unwrap() / distance * attractive_f;
arr[1] += delta.get(1).unwrap() / distance * attractive_f;
}
}
// Limit maximum displacement and prevent being displaced outside frame
10 changes: 7 additions & 3 deletions raphtory/src/python/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ use crate::{
python::graph::node::PyNode,
};
use chrono::NaiveDateTime;
use pyo3::{exceptions::PyTypeError, prelude::*};
use pyo3::{exceptions::PyTypeError, prelude::*, types::PyDateTime};
use std::{future::Future, thread};

pub mod errors;
@@ -71,11 +71,15 @@ impl<'source> FromPyObject<'source> for PyTime {
if let Ok(parsed_datetime) = time.extract::<NaiveDateTime>() {
return Ok(PyTime::new(parsed_datetime.try_into_time()?));
}
let message = format!("time '{time}' must be a str, dt or an integer");
if let Ok(py_datetime) = time.extract::<&PyDateTime>() {
let time = (py_datetime.call_method0("timestamp")?.extract::<f64>()? * 1000.0) as i64;
return Ok(PyTime::new(time));
}

let message = format!("time '{time}' must be a str, datetime or an integer");
Err(PyTypeError::new_err(message))
}
}

impl PyTime {
fn new(parsing_result: i64) -> Self {
Self { parsing_result }

0 comments on commit f9a583c

Please sign in to comment.