Repository: DerwenAI/pynock (language: python)

Issue:
How to split a graph into two partitions?
I am trying to split one graph across two partitions. For a test, I'm loading the karate club graph:

```python
from networkx import karate_club_graph, to_pandas_edgelist

from pynock import Edge, Node, Partition

# create the networkx graph
G = karate_club_graph()
df_expected = to_pandas_edgelist(G)

# create two partitions
partition_0 = Partition(part_id=0)
partition_1 = Partition(part_id=1)

for n, node in enumerate(G.nodes()):
    _temp_node = Node(node_id=node)

    for edge in G[node]:
        e = Edge(node_id=edge)
        _temp_node.add_edge(e)

    if n % 2 == 0:
        partition_0.create_node_name(n)
        partition_0.add_node(_temp_node)
    else:
        partition_1.create_node_name(n)
        partition_1.add_node(_temp_node)

print(list(partition_0.nodes)[:5])
print(list(partition_1.nodes)[:5])

partition_0.to_df()
```

However, I am encountering an error:

```pytb
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In [172], line 30
     27 print(list(partition_0.nodes)[:5])
     28 print(list(partition_1.nodes)[:5])
---> 30 partition_0.to_df()

File ~/pynock/pynock/pynock.py:582, in Partition.to_df(self, sort, debug)
    573     def to_df (
    574         self,
    575         *,
    576         sort: bool = False,
    577         debug: bool = False,
    578         ) -> pd.DataFrame:
    579         """
    580 Represent the partition as a DataFrame.
    581         """
--> 582         return pd.DataFrame([
    583             row
    584             for row in self.iter_gen_rows(
    585                     sort = sort,
    586                     debug = debug,
    587             )
    588         ])

File ~/pynock/pynock/pynock.py:582, in <listcomp>(.0)
    573     def to_df (
    574         self,
    575         *,
    576         sort: bool = False,
    577         debug: bool = False,
    578         ) -> pd.DataFrame:
    579         """
    580 Represent the partition as a DataFrame.
    581         """
--> 582         return pd.DataFrame([
    583             row
    584             for row in self.iter_gen_rows(
    585                     sort = sort,
    586                     debug = debug,
    587             )
    588         ])

File ~/pynock/pynock/pynock.py:561, in Partition.iter_gen_rows(self, sort, debug)
    554     edge_iter = edge_list
    556 for edge in edge_iter:
    557     row = {
    558         "src_name": node.name,
    559         "edge_id": edge_id,
    560         "rel_name": self.edge_rels[edge.rel],
--> 561         "dst_name": self.nodes[edge.node_id].name,
    562         "truth": edge.truth,
    563         "shadow": -1,
    564         "is_rdf": node.is_rdf,
    565         "labels": None,
    566         "props": self._save_props(edge.prop_map, debug=debug),
    567     }
    569     yield row
    570     edge_id += 1

KeyError: 1
```

I roughly understand that the problem is caused by referencing nodes that are defined on another partition, but I'm not sure how to set up the pointer to the relevant partition.


Discussion / hints:

Fix patch:
diff --git a/MANIFEST.in b/MANIFEST.in
index 61f6a04..01c77a9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -5,3 +5,4 @@ include *.txt
 recursive-include dat *.csv
 recursive-include dat *.parq
 recursive-include tests *.py
+recursive-include examples *.ipynb
diff --git a/README.md b/README.md
index 0f664d2..08f0706 100644
--- a/README.md
+++ b/README.md
@@ -91,9 +91,9 @@ python3 cli.py --help
 
 ## Usage programmatically in Python
 
-To construct a partition file programmatically, see the sample code
-[`tiny.py`](https://github.com/DerwenAI/pynock/blob/main/tiny.py)
-which builds the minimal recipe example as an RDF graph.
+To construct a partition file programmatically, see the 
+[`examples`](https://github.com/DerwenAI/pynock/blob/main/examples)
+for Jupyter notebooks with sample code and debugging.
 
 
 ## Background
diff --git a/changelog.txt b/changelog.txt
index 0cb0ae4..f7ecaaf 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,20 +1,29 @@
 # `pynock` changelog
 
+## 1.2.0
+
+2022-10-07
+
+  * migrate sample code to `examples/` Jupyter notebooks
+  * create an example of partitioning based on `NetworkX` "Karate Club"
+  * add convenience methods `Partition.find_or_create_node()` and `Partition.create_edge()`
+
+
 ## 1.1.1
 
 2022-10-06
 
-  * added input validation and warnings
-  * resolved conflicts with `pandas` for missing values
+  * add input validation and warnings
+  * resolve conflicts with `pandas` for missing values
   * consistent parsing and generation of RDF
-  * created CI pipeline using docker and GH Actions
+  * create CI pipeline using docker and GH Actions
 
 
 ## 1.0.1
 
 2022-10-03
 
-  * proposed open standard
+  * propose open standard
 
 
 ## 1.0.0
diff --git a/dat/tiny.csv b/dat/tiny.csv
index f13f434..1680598 100644
--- a/dat/tiny.csv
+++ b/dat/tiny.csv
@@ -1,8 +1,8 @@
 "src_name","edge_id","rel_name","dst_name","truth","shadow","is_rdf","labels","props"
-"http://purl.org/heals/food/Recipe",-1,"","",1.0,-1,True,"",""
-"http://purl.org/heals/ingredient/ChickenEgg",-1,"","",1.0,-1,True,"",""
-"http://purl.org/heals/ingredient/CowMilk",-1,"","",1.0,-1,True,"",""
-"http://purl.org/heals/ingredient/WholeWheatFlour",-1,"","",1.0,-1,True,"",""
+"http://purl.org/heals/food/Recipe",-1,"","",1.0,-1,True,"top_level",""
+"http://purl.org/heals/ingredient/ChickenEgg",-1,"","",1.0,-1,True,"Ingredient",""
+"http://purl.org/heals/ingredient/CowMilk",-1,"","",1.0,-1,True,"Ingredient",""
+"http://purl.org/heals/ingredient/WholeWheatFlour",-1,"","",1.0,-1,True,"Ingredient","{""vegan"":true}"
 "https://www.food.com/recipe/327593",-1,"","",1.0,-1,True,"Recipe","{""minutes"":8,""name"":""anytime crepes""}"
 "https://www.food.com/recipe/327593",0,"http://purl.org/heals/food/uses_ingredient","http://purl.org/heals/ingredient/ChickenEgg",1.0,-1,True,"",""
 "https://www.food.com/recipe/327593",1,"http://purl.org/heals/food/uses_ingredient","http://purl.org/heals/ingredient/CowMilk",1.0,-1,True,"",""
diff --git a/dat/tiny.parq b/dat/tiny.parq
index eb70e13..0e6492b 100644
Binary files a/dat/tiny.parq and b/dat/tiny.parq differ
diff --git a/dat/tiny.ttl b/dat/tiny.ttl
index 707f528..e8d25db 100644
--- a/dat/tiny.ttl
+++ b/dat/tiny.ttl
@@ -1,7 +1,7 @@
-@prefix ind:  <http://purl.org/heals/ingredient/> .
-@prefix wtm:  <http://purl.org/heals/food/> .
+@prefix ns1: <http://purl.org/heals/food/> .
+
+<https://www.food.com/recipe/327593> a ns1:Recipe ;
+    ns1:uses_ingredient <http://purl.org/heals/ingredient/ChickenEgg>,
+        <http://purl.org/heals/ingredient/CowMilk>,
+        <http://purl.org/heals/ingredient/WholeWheatFlour> .
 
-<https://www.food.com/recipe/327593> a wtm:Recipe ;
-    wtm:hasIngredient ind:ChickenEgg,
-        ind:CowMilk,
-        ind:WholeWheatFlour .
diff --git a/examples/karate_club.ipynb b/examples/karate_club.ipynb
new file mode 100644
index 0000000..65a8723
--- /dev/null
+++ b/examples/karate_club.ipynb
@@ -0,0 +1,1081 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "728e5ba2-93a4-4c18-a9ca-ca1489322a76",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# tutorial set up only; do not include this `sys.path` change in production:\n",
+    "import sys ; sys.path.insert(0, \"../\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5dc9faef-21f6-4e65-82d8-0257ab889452",
+   "metadata": {},
+   "source": [
+    "# Karate Club example\n",
+    "\n",
+    "Use the \"Karate Club\" example from `NetworkX` to illustrate creating multiple partitions from one dataset"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a1d36b4e-3ada-4180-88f8-7b080a5f569f",
+   "metadata": {},
+   "source": [
+    "Import the dependencies"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "1c63dec1-d8fc-4174-a2fa-be0a0bfce54e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from icecream import ic\n",
+    "import cloudpathlib\n",
+    "\n",
+    "from networkx import karate_club_graph\n",
+    "\n",
+    "from pynock import Edge, Node, Partition"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5e46b03c-a9db-4e4a-a532-647846cda517",
+   "metadata": {},
+   "source": [
+    "Define a helper method to partition nodes based on a hash of their IDs"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "f4d523ff-0711-4076-b4d1-f88789fcbc44",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def get_part (node_id: int)-> int:\n",
+    "    return node_id % 2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f628642b-ec87-475d-a9b2-fc9e71263193",
+   "metadata": {},
+   "source": [
+    "Create the `NetworkX` graph                                                                                                              "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "f5e4d34a-7e62-4dce-930d-4f946e3fee98",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "G = karate_club_graph()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bb6c85ec-ba44-4621-82eb-a32cad1c4381",
+   "metadata": {},
+   "source": [
+    "Create two `NOCK` partitions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "d76cbb95-4542-4637-a0dc-48f1beeba64c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "partition = [\n",
+    "    Partition(part_id = 0),\n",
+    "    Partition(part_id = 1),\n",
+    "]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "4456e738-3266-4097-9dca-23132d736b2a",
+   "metadata": {},
+   "source": [
+    "Build the NOCK partitions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bfb8db5c-eefd-4039-bba8-ba4b950453bf",
+   "metadata": {
+    "scrolled": true,
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "add src node 00 in part 0\n",
+      "0 [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 10), (0, 11), (0, 12), (0, 13), (0, 17), (0, 19), (0, 21), (0, 31)]\n",
+      " dst node 01 in part 0, shadow 0\n",
+      " edge 00: 0 => 01\n",
+      " dst node 02 in part 0, shadow -1\n",
+      " edge 00: 0 => 02\n",
+      " dst node 03 in part 0, shadow 0\n",
+      " edge 00: 0 => 03\n",
+      " dst node 04 in part 0, shadow -1\n",
+      " edge 00: 0 => 04\n",
+      " dst node 05 in part 0, shadow 0\n",
+      " edge 00: 0 => 05\n",
+      " dst node 06 in part 0, shadow -1\n",
+      " edge 00: 0 => 06\n",
+      " dst node 07 in part 0, shadow 0\n",
+      " edge 00: 0 => 07\n",
+      " dst node 08 in part 0, shadow -1\n",
+      " edge 00: 0 => 08\n",
+      " dst node 10 in part 0, shadow -1\n",
+      " edge 00: 0 => 10\n",
+      " dst node 11 in part 0, shadow 0\n",
+      " edge 00: 0 => 11\n",
+      " dst node 12 in part 0, shadow -1\n",
+      " edge 00: 0 => 12\n",
+      " dst node 13 in part 0, shadow 0\n",
+      " edge 00: 0 => 13\n",
+      " dst node 17 in part 0, shadow 0\n",
+      " edge 00: 0 => 17\n",
+      " dst node 19 in part 0, shadow 0\n",
+      " edge 00: 0 => 19\n",
+      " dst node 21 in part 0, shadow 0\n",
+      " edge 00: 0 => 21\n",
+      " dst node 31 in part 0, shadow 0\n",
+      " edge 00: 0 => 31\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "ic| node: Node(node_id=0, name='00', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=1, truth=1.0, prop_map={}), Edge(rel=0, node_id=2, truth=1.0, prop_map={}), Edge(rel=0, node_id=3, truth=1.0, prop_map={}), Edge(rel=0, node_id=4, truth=1.0, prop_map={}), Edge(rel=0, node_id=5, truth=1.0, prop_map={}), Edge(rel=0, node_id=6, truth=1.0, prop_map={}), Edge(rel=0, node_id=7, truth=1.0, prop_map={}), Edge(rel=0, node_id=8, truth=1.0, prop_map={}), Edge(rel=0, node_id=9, truth=1.0, prop_map={}), Edge(rel=0, node_id=10, truth=1.0, prop_map={}), Edge(rel=0, node_id=11, truth=1.0, prop_map={}), Edge(rel=0, node_id=12, truth=1.0, prop_map={}), Edge(rel=0, node_id=13, truth=1.0, prop_map={}), Edge(rel=0, node_id=14, truth=1.0, prop_map={}), Edge(rel=0, node_id=15, truth=1.0, prop_map={}), Edge(rel=0, node_id=16, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=1, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '01'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=2, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '02'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=3, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '03'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=4, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '04'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=5, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '05'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=6, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '06'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=7, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '07'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=8, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '08'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=9, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '10'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=10, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '11'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=11, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '12'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=12, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '13'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=13, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '17'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=14, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '19'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=15, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '21'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '31'\n",
+      "ic| node: Node(node_id=0, name='01', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=1, truth=1.0, prop_map={}), Edge(rel=0, node_id=2, truth=1.0, prop_map={}), Edge(rel=0, node_id=3, truth=1.0, prop_map={}), Edge(rel=0, node_id=4, truth=1.0, prop_map={}), Edge(rel=0, node_id=5, truth=1.0, prop_map={}), Edge(rel=0, node_id=6"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "add src node 01 in part 1\n",
+      "0 [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 10), (0, 11), (0, 12), (0, 13), (0, 17), (0, 19), (0, 21), (0, 31)]\n",
+      " dst node 01 in part 1, shadow 0\n",
+      " edge 01: 0 => 01\n",
+      " dst node 02 in part 1, shadow 0\n",
+      " edge 01: 0 => 02\n",
+      " dst node 03 in part 1, shadow -1\n",
+      " edge 01: 0 => 03\n",
+      " dst node 04 in part 1, shadow 0\n",
+      " edge 01: 0 => 04\n",
+      " dst node 05 in part 1, shadow -1\n",
+      " edge 01: 0 => 05\n",
+      " dst node 06 in part 1, shadow 0\n",
+      " edge 01: 0 => 06\n",
+      " dst node 07 in part 1, shadow -1\n",
+      " edge 01: 0 => 07\n",
+      " dst node 08 in part 1, shadow 0\n",
+      " edge 01: 0 => 08\n",
+      " dst node 10 in part 1, shadow 0\n",
+      " edge 01: 0 => 10\n",
+      " dst node 11 in part 1, shadow -1\n",
+      " edge 01: 0 => 11\n",
+      " dst node 12 in part 1, shadow 0\n",
+      " edge 01: 0 => 12\n",
+      " dst node 13 in part 1, shadow -1\n",
+      " edge 01: 0 => 13\n",
+      " dst node 17 in part 1, shadow -1\n",
+      " edge 01: 0 => 17\n",
+      " dst node 19 in part 1, shadow -1\n",
+      " edge 01: 0 => 19\n",
+      " dst node 21 in part 1, shadow -1\n",
+      " edge 01: 0 => 21\n",
+      " dst node 31 in part 1, shadow -1\n",
+      " edge 01: 0 => 31\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      ", truth=1.0, prop_map={}), Edge(rel=0, node_id=7, truth=1.0, prop_map={}), Edge(rel=0, node_id=8, truth=1.0, prop_map={}), Edge(rel=0, node_id=9, truth=1.0, prop_map={}), Edge(rel=0, node_id=10, truth=1.0, prop_map={}), Edge(rel=0, node_id=11, truth=1.0, prop_map={}), Edge(rel=0, node_id=12, truth=1.0, prop_map={}), Edge(rel=0, node_id=13, truth=1.0, prop_map={}), Edge(rel=0, node_id=14, truth=1.0, prop_map={}), Edge(rel=0, node_id=15, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=0, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '01'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=1, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '02'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=2, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '03'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=3, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '04'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=4, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '05'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=5, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '06'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=6, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '07'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=7, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '08'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=8, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '10'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=9, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '11'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=10, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '12'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=11, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '13'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=12, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '17'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=13, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '19'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=14, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '21'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=15, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '31'\n",
+      "ic| node: Node(node_id=2, name='02', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=1, truth=1.0, prop_map={}), Edge(rel=0, node_id=3, truth=1.0, prop_map={}), Edge(rel=0, node_id=7, truth=1.0, prop_map={}), Edge(rel=0, node_id=8, truth=1.0, prop_map={}), Edge(rel=0, node_id=17, truth=1.0, prop_map={}), Edge(rel=0, node_id=12, truth=1.0, prop_map={}), Edge(rel=0, node_id=18, truth=1.0, prop_map={}), Edge(rel=0, node_id=19, truth=1.0, prop_map={}), Edge(rel=0, node_id=20, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=0, truth=1.0, prop_map={"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "2 [(2, 0), (2, 1), (2, 3), (2, 7), (2, 8), (2, 9), (2, 13), (2, 27), (2, 28), (2, 32)]\n",
+      " dst node 00 in part 0, shadow -1\n",
+      " edge 02: 2 => 00\n",
+      " dst node 01 in part 0, shadow -1\n",
+      " edge 02: 2 => 01\n",
+      " dst node 03 in part 0, shadow -1\n",
+      " edge 02: 2 => 03\n",
+      " dst node 07 in part 0, shadow -1\n",
+      " edge 02: 2 => 07\n",
+      " dst node 08 in part 0, shadow -1\n",
+      " edge 02: 2 => 08\n",
+      " dst node 09 in part 0, shadow 0\n",
+      " edge 02: 2 => 09\n",
+      " dst node 13 in part 0, shadow 0\n",
+      " edge 02: 2 => 13\n",
+      " dst node 27 in part 0, shadow 0\n",
+      " edge 02: 2 => 27\n",
+      " dst node 28 in part 0, shadow -1\n",
+      " edge 02: 2 => 28\n",
+      " dst node 32 in part 0, shadow -1\n",
+      " edge 02: 2 => 32\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=1, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '01'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=3, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '03'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=7, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '07'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=8, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '08'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=17, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '09'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=12, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '13'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=18, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '27'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=19, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '28'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=20, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '32'\n",
+      "ic| node: Node(node_id=2, name='03', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=16, truth=1.0, prop_map={}), Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=2, truth=1.0, prop_map={}), Edge(rel=0, node_id=6, truth=1.0, prop_map={}), Edge(rel=0, node_id=7, truth=1.0, prop_map={}), Edge(rel=0, node_id=17, truth=1.0, prop_map={}), Edge(rel=0, node_id=11, truth=1.0, prop_map={}), Edge(rel=0, node_id=18, truth=1.0, prop_map={}), Edge(rel=0, node_id=19, truth=1.0, prop_map={}), Edge(rel=0, node_id=20, truth=1.0, "
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "2 [(2, 0), (2, 1), (2, 3), (2, 7), (2, 8), (2, 9), (2, 13), (2, 27), (2, 28), (2, 32)]\n",
+      " dst node 00 in part 1, shadow 0\n",
+      " edge 03: 2 => 00\n",
+      " dst node 01 in part 1, shadow 0\n",
+      " edge 03: 2 => 01\n",
+      " dst node 03 in part 1, shadow 0\n",
+      " edge 03: 2 => 03\n",
+      " dst node 07 in part 1, shadow 0\n",
+      " edge 03: 2 => 07\n",
+      " dst node 08 in part 1, shadow 0\n",
+      " edge 03: 2 => 08\n",
+      " dst node 09 in part 1, shadow -1\n",
+      " edge 03: 2 => 09\n",
+      " dst node 13 in part 1, shadow -1\n",
+      " edge 03: 2 => 13\n",
+      " dst node 27 in part 1, shadow -1\n",
+      " edge 03: 2 => 27\n",
+      " dst node 28 in part 1, shadow 0\n",
+      " edge 03: 2 => 28\n",
+      " dst node 32 in part 1, shadow 0\n",
+      " edge 03: 2 => 32\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=0, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '01'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=2, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '03'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=6, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '07'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=7, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '08'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=17, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '09'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=11, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '13'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=18, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '27'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=19, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '28'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=20, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '32'\n",
+      "ic| node: Node(node_id=4, name='04', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=6, truth=1.0, prop_map={}), Edge(rel=0, node_id=9, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=0, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=6, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '06'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=9, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '10'\n",
+      "ic| node: Node(node_id=4, name='05', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=16, truth=1.0, prop_map="
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "4 [(4, 0), (4, 6), (4, 10)]\n",
+      " dst node 00 in part 0, shadow 0\n",
+      " edge 04: 4 => 00\n",
+      " dst node 06 in part 0, shadow 0\n",
+      " edge 04: 4 => 06\n",
+      " dst node 10 in part 0, shadow 0\n",
+      " edge 04: 4 => 10\n",
+      "4 [(4, 0), (4, 6), (4, 10)]\n",
+      " dst node 00 in part 1, shadow 0\n",
+      " edge 05: 4 => 00\n",
+      " dst node 06 in part 1, shadow 0\n",
+      " edge 05: 4 => 06\n",
+      " dst node 10 in part 1, shadow 0\n",
+      " edge 05: 4 => 10\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "{}), Edge(rel=0, node_id=5, truth=1.0, prop_map={}), Edge(rel=0, node_id=8, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=5, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '06'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=8, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '10'\n",
+      "ic| node: Node(node_id=6, name='06', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=4, truth=1.0, prop_map={}), Edge(rel=0, node_id=5"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "6 [(6, 0), (6, 4), (6, 5), (6, 16)]\n",
+      " dst node 00 in part 0, shadow 0\n",
+      " edge 06: 6 => 00\n",
+      " dst node 04 in part 0, shadow 0\n",
+      " edge 06: 6 => 04\n",
+      " dst node 05 in part 0, shadow 0\n",
+      " edge 06: 6 => 05\n",
+      " dst node 16 in part 0, shadow -1\n",
+      " edge 06: 6 => 16\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "0, node_id=21, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '16'\n",
+      "ic| node: Node(node_id=6, name='07', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=16, truth=1.0, prop_map={}), Edge(rel=0, node_id=3, truth=1.0, prop_map={}), Edge(rel=0, node_id=4, truth=1.0, prop_map={}), Edge(rel=0, node_id=21, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=3, truth=1.0, prop_map={})\n",
+      "    dst_node.name: "
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "6 [(6, 0), (6, 4), (6, 5), (6, 16)]\n",
+      " dst node 00 in part 1, shadow -1\n",
+      " edge 07: 6 => 00\n",
+      " dst node 04 in part 1, shadow -1\n",
+      " edge 07: 6 => 04\n",
+      " dst node 05 in part 1, shadow -1\n",
+      " edge 07: 6 => 05\n",
+      " dst node 16 in part 1, shadow 0\n",
+      " edge 07: 6 => 16\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "'04'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=4, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '05'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=21, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '16'\n",
+      "ic| node: Node(node_id=8, name='08', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=2, truth=1.0, prop_map={}), Edge(rel=0, node_id=22, truth=1.0, prop_map={}), Edge(rel=0, : Edge(rel=0, node_id=2, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '02'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=23, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=9, name='11', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=1, truth=1.0, prop_map={}), Edge(rel=0, node_id=22, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=1, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '02'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=22, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=11, name='12', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=0, truth=1.0, prop_map={})\n",
+      "    dst_node.name: "
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "9 [(9, 2), (9, 33)]\n",
+      " dst node 02 in part 1, shadow 0\n",
+      " edge 11: 9 => 02\n",
+      " dst node 33 in part 1, shadow -1\n",
+      " edge 11: 9 => 33\n",
+      "11 [(11, 0)]\n",
+      " dst node 00 in part 0, shadow -1\n",
+      " edge 12: 11 => 00\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "'00'\n",
+      "ic| node: Node(node_id=11, name='13', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=16, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| node: Node(node_id=24, name='14', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=25, truth=1.0, prop_map={}), Edge(rel=0, node_id=18, truth=1.0, prop_map={}), Edge(rel=0, node_id=16, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "11 [(11, 0)]\n",
+      " dst node 00 in part 1, shadow -1\n",
+      " edge 13: 11 => 00\n",
+      "add src node 14 in part 0\n",
+      "24 [(24, 25), (24, 27), (24, 31)]\n",
+      " dst node 25 in part 0, shadow 0\n",
+      " edge 14: 24 => 25\n",
+      " dst node 27 in part 0, shadow 0\n",
+      " edge 14: 24 => 27\n",
+      " dst node 31 in part 0, shadow 0\n",
+      " edge 14: 24 => 31\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "=0, node_id=25, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '25'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=18, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '27'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '31'\n",
+      "ic| node: Node(node_id=23, name='15', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=24, truth=1.0, prop_map={}), Edge(rel=0, node_id=18, truth=1.0, prop_map={}), Edge(rel=0, node_id=25, truth=1.0, prop_map={}), Edge(rel=0, node_id=20, truth=1.0, prop_map={}), Edge(rel=0, node_id=22, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=24, truth1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=1, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=0, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=1, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '01'\n",
+      "ic| node: Node(node_id=12, name='17', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=16, truth=1.0, prop_map={}), Edge(rel=0, node_id=2, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=2, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '03'\n",
+      "ic| node: Node(node_id=26, name='18', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=27, truth=1.0, prop_map={}), Edge(rel=0, node_id=23, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=27, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '29'\n",
+      "ic| edge_rel: 0"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "12 [(12, 0), (12, 3)]\n",
+      " dst node 00 in part 1, shadow -1\n",
+      " edge 17: 12 => 00\n",
+      " dst node 03 in part 1, shadow -1\n",
+      " edge 17: 12 => 03\n",
+      "add src node 18 in part 0\n",
+      "26 [(26, 29), (26, 33)]\n",
+      " dst node 29 in part 0, shadow 0\n",
+      " edge 18: 26 => 29\n",
+      " dst node 33 in part 0, shadow 0\n",
+      " edge 18: 26 => 33\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "    edge: Edge(rel=0, node_id=23, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=13, name='19', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=16, truth=1.0, prop_map={}), Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=1, truth=1.0, prop_map={}), Edge(rel=0, node_id=2, truth=1.0, prop_map={}), Edge(rel=0, node_id=22, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '00'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=0, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '01'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=1, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '02'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id="
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "13 [(13, 0), (13, 1), (13, 2), (13, 3), (13, 33)]\n",
+      " dst node 00 in part 1, shadow 0\n",
+      " edge 19: 13 => 00\n",
+      " dst node 01 in part 1, shadow 0\n",
+      " edge 19: 13 => 01\n",
+      " dst node 02 in part 1, shadow 0\n",
+      " edge 19: 13 => 02\n",
+      " dst node 03 in part 1, shadow 0\n",
+      " edge 19: 13 => 03\n",
+      " dst node 33 in part 1, shadow 0\n",
+      " edge 19: 13 => 33\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "2, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '03'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=22, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=28, name='20', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=2, truth=1.0, prop_map={}), Edge(rel=0, node_id=16, truth=1.0, prop_map={}), Edge(rel=0, node_id=23, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=2, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '02'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '31'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=23, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=14, name='21', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=20, truth=1.0, prop_map={}), Edge(rel=0, node_id=22, truth=1.0, prop_map={})]})\n",
+      "ic"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "add src node 20 in part 0\n",
+      "28 [(28, 2), (28, 31), (28, 33)]\n",
+      " dst node 02 in part 0, shadow 0\n",
+      " edge 20: 28 => 02\n",
+      " dst node 31 in part 0, shadow 0\n",
+      " edge 20: 28 => 31\n",
+      " dst node 33 in part 0, shadow 0\n",
+      " edge 20: 28 => 33\n",
+      "14 [(14, 32), (14, 33)]\n",
+      " dst node 32 in part 1, shadow 0\n",
+      " edge 21: 14 => 32\n",
+      " dst node 33 in part 1, shadow 0\n",
+      " edge 21: 14 => 33\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=20, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '32'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=22, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=29, name='22', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=30, truth=1.0, prop_map={}), Edge(rel=0(rel=0, node_id=23, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=2, truth=1.0, "
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "add src node 24 in part 0\n",
+      "32 [(32, 2), (32, 8), (32, 14), (32, 15), (32, 18), (32, 20), (32, 22), (32, 23), (32, 29), (32, 30), (32, 31), (32, 33)]\n",
+      " dst node 02 in part 0, shadow -1\n",
+      " edge 24: 32 => 02\n",
+      " dst node 08 in part 0, shadow -1\n",
+      " edge 24: 32 => 08\n",
+      " dst node 14 in part 0, shadow -1\n",
+      " edge 24: 32 => 14\n",
+      " dst node 15 in part 0, shadow 0\n",
+      " edge 24: 32 => 15\n",
+      " dst node 18 in part 0, shadow 0\n",
+      " edge 24: 32 => 18\n",
+      " dst node 20 in part 0, shadow 0\n",
+      " edge 24: 32 => 20\n",
+      " dst node 22 in part 0, shadow 0\n",
+      " edge 24: 32 => 22\n",
+      " dst node 23 in part 0, shadow 0\n",
+      " edge 24: 32 => 23\n",
+      " dst node 29 in part 0, shadow 0\n",
+      " edge 24: 32 => 29\n",
+      " dst node 30 in part 0, shadow 0\n",
+      " edge 24: 32 => 30\n",
+      " dst node 31 in part 0, shadow 0\n",
+      " edge 24: 32 => 31\n",
+      " dst node 33 in part 0, shadow 0\n",
+      " edge 24: 32 => 33\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "prop_map={})\n",
+      "    dst_node.name: '02'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=8, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '08'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=24, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '14'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=33, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '15'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=26, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '18'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=28, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '20'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=29, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '22'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=30, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '23'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=27, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '29'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=22, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '30'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=16, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '31'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=23, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=24, name='25', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=24, truth=1.0, prop_map={}), Edge(rel=0, node_id=18, truth=1.0, prop_map={}), Edge(rel=0, node_id=15, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=24, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '25'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=18, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '27'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=15, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '31'\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "24 [(24, 25), (24, 27), (24, 31)]\n",
+      " dst node 25 in part 1, shadow 0\n",
+      " edge 25: 24 => 25\n",
+      " dst node 27 in part 1, shadow 0\n",
+      " edge 25: 24 => 27\n",
+      " dst node 31 in part 1, shadow 0\n",
+      " edge 25: 24 => 31\n",
+      "31 [(31, 0), (31, 24), (31, 25), (31, 28), (31, 32), (31, 33)]\n",
+      " dst node 00 in part 0, shadow 0\n",
+      " edge 26: 31 => 00\n",
+      " dst node 24 in part 0, shadow 0\n",
+      " edge 26: 31 => 24\n",
+      " dst node 25 in part 0, shadow 0"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "ic| node: Node(node_id=31, name='26', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={}), Edge(rel=0, node_id=32, truth=1.0, prop_map={}), Edge(rel=0, node_id=25, truth=1.0, prop_map={}), Edge(rel=0, node_id=19, truth=1.0, prop_map={}), Edge(rel=0, node_id=20, truth=1.0,ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=32, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '24'"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      " edge 26: 31 => 25\n",
+      " dst node 28 in part 0, shadow 0\n",
+      " edge 26: 31 => 28\n",
+      " dst node 32 in part 0, shadow 0\n",
+      " edge 26: 31 => 32\n",
+      " dst node 33 in part 0, shadow 0\n",
+      " edge 26: 31 => 33\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=25, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '25'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=19, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '28'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=20, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '32'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=23, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=18, name='27', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=20, truth=1.0, prop_map={}), Edge(rel=0, node_id=22, truth=1.0, prop_map={})]})\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=20, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '32'\n",
+      "ic| edge_rel: 0\n",
+      "    edge: Edge(rel=0, node_id=22, truth=1.0, prop_map={})\n",
+      "    dst_node.name: '33'\n",
+      "ic| node: Node(node_id=19, name='28', shadow=-1, is_rdf=False, label_set=set(), truth=1.0, prop_map={}, edge_map={0: [Edge(rel=0, node_id=0, truth=1.0, prop_map={})"
+     ]
+    }
+   ],
+   "source": [
+    "for src_node_id in G.nodes():\n",
+    "    # round-robin to partition on the src ID                                                                                             \n",
+    "    part_id: int = get_part(src_node_id)\n",
+    "    part = partition[part_id]\n",
+    "\n",
+    "    # lookup/create the src node                                                                                                         \n",
+    "    src_name: str = str(src_node_id).zfill(2)\n",
+    "    src_node = part.lookup_node(src_name)\n",
+    "\n",
+    "    if src_node is None:\n",
+    "        src_node = Node(\n",
+    "            node_id = part.create_node_name(src_name),\n",
+    "            name = src_name,\n",
+    "        )\n",
+    "\n",
+    "        part.add_node(src_node)\n",
+    "        print(f\"add src node { src_node.name } in part { part.part_id }\")\n",
+    "\n",
+    "    # for each edge ...                                                                                                                  \n",
+    "    print(src_node.node_id, G.edges(src_node.node_id))\n",
+    "\n",
+    "    for _, dst_node_id in G.edges(src_node.node_id):\n",
+    "        # lookup/create the dst node                                                                                                     \n",
+    "        dst_name: str = str(dst_node_id).zfill(2)\n",
+    "        dst_node = part.lookup_node(dst_name)\n",
+    "\n",
+    "        if dst_node is None:\n",
+    "            shadow: int = Node.BASED_LOCAL\n",
+    "\n",
+    "            if part.part_id != get_part(dst_node_id):\n",
+    "                shadow = 0\n",
+    "\n",
+    "            dst_node = Node(\n",
+    "                node_id = part.create_node_name(dst_name),\n",
+    "                name = dst_name,\n",
+    "                shadow = shadow,\n",
+    "            )\n",
+    "\n",
+    "            part.add_node(dst_node)\n",
+    "\n",
+    "        print(f\" dst node { dst_node.name } in part { part.part_id }, shadow { shadow }\")\n",
+    "\n",
+    "        # define an edge connecting src => dst                                                                                           \n",
+    "        edge: Edge = Edge(\n",
+    "            rel = Edge.BLANK_RELATION,\n",
+    "            node_id = dst_node.node_id,\n",
+    "        )\n",
+    "\n",
+    "        src_node.add_edge(edge)\n",
+    "        print(f\" edge { src_node.name }: { src_node.node_id } => { dst_node.name }\")\n",
+    "\n",
+    "    part.dump_node(src_node)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b9317b01-b1ea-408e-afb1-b8a6e63a5b2d",
+   "metadata": {},
+   "source": [
+    "Extract the names for non-shadow node, i.e., compare with the expected Karate Club node IDs"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "9a3effc9-2570-46d5-8179-3953089d60fc",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "part 0 ['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22', '24', '26', '28', '30', '32']\n",
+      "part 1 ['01', '03', '05', '07', '09', '11', '13', '15', '17', '19', '21', '23', '25', '27', '29', '31', '33']\n"
+     ]
+    }
+   ],
+   "source": [
+    "for part in partition:\n",
+    "    karate = sorted([\n",
+    "        src_node.name\n",
+    "        for src_node in part.nodes.values()\n",
+    "        if src_node.shadow == Node.BASED_LOCAL\n",
+    "    ])\n",
+    "\n",
+    "    print(f\"part { part.part_id }\", karate)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3bccb4ce-c11a-40cb-81ba-c805424e20f0",
+   "metadata": {},
+   "source": [
+    "Save the partitions to CSV files"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "fc45a6ec-995f-41fb-a35f-02b6ab9320b2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for part in partition:\n",
+    "    part.save_file_csv(\n",
+    "        cloudpathlib.AnyPath(f\"part_{ part.part_id }.csv\"),\n",
+    "        sort = True,\n",
+    "    )"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7ceab1a7-7446-4d12-b3ec-5386e722fc5e",
+   "metadata": {},
+   "source": [
+    "Dump to dataframes"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "acc4447a-2071-483c-aa21-57334da44b96",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "ic| part.to_df().head():   src_name  edge_id rel_name dst_name  truth  shadow  is_rdf labels props\n",
+      "                         0       00       -1     None     None    1.0      -1   False             \n",
+      "                         1       00        0                01    1.0      -1   False   None      \n",
+      "                         2       00        1                02    1.0      -1   False   None      \n",
+      "                         3       00        2                03    1.0      -1   False   None      \n",
+      "                         4       00        3                04    1.0      -1   False   None      \n",
+      "ic| part.to_df().head():   src_name  edge_id rel_name dst_name  truth  shadow  is_rdf labels props\n",
+      "                         0       01       -1     None     None    1.0      -1   False             \n",
+      "                         1       01        0                01    1.0      -1   False   None      \n",
+      "                         2       01        1                02    1.0      -1   False   None      \n",
+      "                         3       01        2                03    1.0      -1   False   None      \n",
+      "                         4       01        3                04    1.0      -1   False   None      \n"
+     ]
+    }
+   ],
+   "source": [
+    "for part in partition:\n",
+    "    ic(part.to_df().head())"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/tiny.ipynb b/examples/tiny.ipynb
new file mode 100644
index 0000000..a0c4d7f
--- /dev/null
+++ b/examples/tiny.ipynb
@@ -0,0 +1,581 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "728e5ba2-93a4-4c18-a9ca-ca1489322a76",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# tutorial set up only; do not include this `sys.path` change in production:\n",
+    "import sys ; sys.path.insert(0, \"../\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5dc9faef-21f6-4e65-82d8-0257ab889452",
+   "metadata": {},
+   "source": [
+    "# Minimal Example\n",
+    "\n",
+    "A minimal example of how to build a `NOCK` partition programmatically.\n",
+    "This generates the `dat/tiny.*` files, based on the recipe for [_Anytime Crepes_](https://www.food.com/recipe/327593) on Food.com"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a1d36b4e-3ada-4180-88f8-7b080a5f569f",
+   "metadata": {},
+   "source": [
+    "Import the dependencies"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "1c63dec1-d8fc-4174-a2fa-be0a0bfce54e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from icecream import ic\n",
+    "import cloudpathlib\n",
+    "\n",
+    "from pynock import Edge, Node, Partition"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bb6c85ec-ba44-4621-82eb-a32cad1c4381",
+   "metadata": {},
+   "source": [
+    "Create the partition"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "d76cbb95-4542-4637-a0dc-48f1beeba64c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "part: Partition = Partition(\n",
+    "    part_id = 0,\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "4456e738-3266-4097-9dca-23132d736b2a",
+   "metadata": {},
+   "source": [
+    "Perform lookup/create for the `src` node for the `\"Anytime Crepes\"` recipe\n",
+    "\n",
+    "NB: this node has properties, which RDF cannot access"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "e5b16e62-75b6-4e5a-8f38-d2ff5550947b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "src_name: str = \"https://www.food.com/recipe/327593\"\n",
+    "src_node: Node = part.find_or_create_node(src_name)\n",
+    "\n",
+    "src_node.is_rdf = True\n",
+    "src_node.label_set = set([\"Recipe\"])\n",
+    "src_node.prop_map = {\n",
+    "    \"minutes\": 8,\n",
+    "    \"name\": \"anytime crepes\",\n",
+    "}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7f967f77-2e97-4866-9878-7bd1176d6c91",
+   "metadata": {},
+   "source": [
+    "Perform lookup/create for the `dst` node for the `\"Egg\"` ingredient"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "498e0ce7-f411-4d4e-8704-804dd57d6bfc",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dst_name: str = \"http://purl.org/heals/ingredient/ChickenEgg\"\n",
+    "dst_node: Node = part.find_or_create_node(dst_name)\n",
+    "\n",
+    "dst_node.is_rdf = True\n",
+    "dst_node.label_set = set([\"Ingredient\"])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f9433aba-36c0-4c21-a757-d3ab3ac74a58",
+   "metadata": {},
+   "source": [
+    "Define an edge connecting `src` => `dst` for this ingredient"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "acbfa7b1-60cd-44b3-90eb-3eb47be78fd9",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Edge(rel=1, node_id=1, truth=1.0, prop_map={})"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "part.create_edge(\n",
+    "    src_node,\n",
+    "    \"http://purl.org/heals/food/uses_ingredient\",\n",
+    "    dst_node,\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9b28e939-c436-4d9b-93c1-2919a5ac3749",
+   "metadata": {},
+   "source": [
+    "Perform lookup/create for the `dst` node for the `\"Milk\"` ingredient"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "31bc503c-974d-46b5-bc99-d395c6b15b17",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dst_name = \"http://purl.org/heals/ingredient/CowMilk\"\n",
+    "dst_node = part.find_or_create_node(dst_name)\n",
+    "\n",
+    "dst_node.is_rdf = True\n",
+    "dst_node.label_set = set([\"Ingredient\"])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "17aaed94-531c-4808-86ab-fb4c1d85b7a4",
+   "metadata": {},
+   "source": [
+    "Define an edge connecting `src` => `dst` for this ingredient"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "aaab36b3-4b27-44f1-a2d3-80a79ce7dcae",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Edge(rel=1, node_id=2, truth=1.0, prop_map={})"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "part.create_edge(\n",
+    "    src_node,\n",
+    "    \"http://purl.org/heals/food/uses_ingredient\",\n",
+    "    dst_node,\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "fa60fb59-483b-4b59-b6e6-a4f5b3f793d6",
+   "metadata": {},
+   "source": [
+    "Perform lookup/create for the `dst` node for the `\"Flour\"` ingredient\n",
+    "\n",
+    "NB: this node has properties, which RDF cannot access "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "2015e03b-3394-4a1c-9166-04ddd108cd0c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dst_name = \"http://purl.org/heals/ingredient/WholeWheatFlour\"\n",
+    "dst_node = part.find_or_create_node(dst_name)\n",
+    "\n",
+    "dst_node.is_rdf = True\n",
+    "dst_node.label_set = set([\"Ingredient\"])\n",
+    "dst_node.prop_map = {\n",
+    "    \"vegan\": True,\n",
+    "}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "49aaa2b2-d14e-4dfe-a387-4e77080c3e90",
+   "metadata": {},
+   "source": [
+    "Define an edge connecting `src` => `dst` for this ingredient"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "2f63c3c0-2cf5-4e66-bcf9-0e62664ffa66",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Edge(rel=1, node_id=3, truth=1.0, prop_map={})"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "part.create_edge(\n",
+    "    src_node,\n",
+    "    \"http://purl.org/heals/food/uses_ingredient\",\n",
+    "    dst_node,\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "98b7ec24-d946-4351-b264-a018dfc4f655",
+   "metadata": {},
+   "source": [
+    "Perform lookup/create for the `dst` node for the `\"wtm:Recipe\"` parent"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "855ccd96-8dab-43e0-ba78-d2d11f23bac5",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dst_name = \"http://purl.org/heals/food/Recipe\"\n",
+    "dst_node = part.find_or_create_node(dst_name)\n",
+    "\n",
+    "dst_node.is_rdf = True\n",
+    "dst_node.label_set = set([\"top_level\"])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0b0c9e16-314a-4863-89ac-41f1b94f4c57",
+   "metadata": {},
+   "source": [
+    "Define an edge connecting `src` => `dst` for this inheritance"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "6a4bfe05-182b-45d8-b431-5037762b8691",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Edge(rel=2, node_id=4, truth=1.0, prop_map={})"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "part.create_edge(\n",
+    "    src_node,\n",
+    "    \"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\",\n",
+    "    dst_node,\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "854e0c46-ba3e-4905-b82a-f657dc82cb62",
+   "metadata": {},
+   "source": [
+    "Serialize the partition to multiple formats"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "a5094c68-24b2-4ba6-8659-15e4249f78bd",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "part.save_file_parquet(\n",
+    "    cloudpathlib.AnyPath(\"foo.parq\"),\n",
+    ")\n",
+    "\n",
+    "part.save_file_csv(\n",
+    "    cloudpathlib.AnyPath(\"foo.csv\"),\n",
+    "    sort = True,\n",
+    ")\n",
+    "\n",
+    "part.save_file_rdf(\n",
+    "    cloudpathlib.AnyPath(\"foo.ttl\"),\n",
+    "    rdf_format = \"ttl\",\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "75dff41d-977b-41fd-ad83-e4ed137d077d",
+   "metadata": {},
+   "source": [
+    "Check the files \"foo.*\" to see what was constructed programmatically"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "723cfeef-d782-4874-8669-5a1b4aa711e8",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "@prefix ns1: <http://purl.org/heals/food/> .\n",
+      "\n",
+      "<https://www.food.com/recipe/327593> a ns1:Recipe ;\n",
+      "    ns1:uses_ingredient <http://purl.org/heals/ingredient/ChickenEgg>,\n",
+      "        <http://purl.org/heals/ingredient/CowMilk>,\n",
+      "        <http://purl.org/heals/ingredient/WholeWheatFlour> .\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "!cat foo.ttl"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "id": "8d32907a-52c4-4531-aa7d-9ce23140c50d",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\"src_name\",\"edge_id\",\"rel_name\",\"dst_name\",\"truth\",\"shadow\",\"is_rdf\",\"labels\",\"props\"\n",
+      "\"http://purl.org/heals/food/Recipe\",-1,\"\",\"\",1.0,-1,True,\"top_level\",\"\"\n",
+      "\"http://purl.org/heals/ingredient/ChickenEgg\",-1,\"\",\"\",1.0,-1,True,\"Ingredient\",\"\"\n",
+      "\"http://purl.org/heals/ingredient/CowMilk\",-1,\"\",\"\",1.0,-1,True,\"Ingredient\",\"\"\n",
+      "\"http://purl.org/heals/ingredient/WholeWheatFlour\",-1,\"\",\"\",1.0,-1,True,\"Ingredient\",\"{\"\"vegan\"\":true}\"\n",
+      "\"https://www.food.com/recipe/327593\",-1,\"\",\"\",1.0,-1,True,\"Recipe\",\"{\"\"minutes\"\":8,\"\"name\"\":\"\"anytime crepes\"\"}\"\n",
+      "\"https://www.food.com/recipe/327593\",0,\"http://purl.org/heals/food/uses_ingredient\",\"http://purl.org/heals/ingredient/ChickenEgg\",1.0,-1,True,\"\",\"\"\n",
+      "\"https://www.food.com/recipe/327593\",1,\"http://purl.org/heals/food/uses_ingredient\",\"http://purl.org/heals/ingredient/CowMilk\",1.0,-1,True,\"\",\"\"\n",
+      "\"https://www.food.com/recipe/327593\",2,\"http://purl.org/heals/food/uses_ingredient\",\"http://purl.org/heals/ingredient/WholeWheatFlour\",1.0,-1,True,\"\",\"\"\n",
+      "\"https://www.food.com/recipe/327593\",3,\"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\",\"http://purl.org/heals/food/Recipe\",1.0,-1,True,\"\",\"\"\n"
+     ]
+    }
+   ],
+   "source": [
+    "!cat foo.csv"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8d92a6fc-cc88-49bc-b42c-291d6e5d9372",
+   "metadata": {},
+   "source": [
+    "Show the dataframe representation"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "id": "6afba607-81a5-4ad2-b71e-2e6f6fa5052d",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>src_name</th>\n",
+       "      <th>edge_id</th>\n",
+       "      <th>rel_name</th>\n",
+       "      <th>dst_name</th>\n",
+       "      <th>truth</th>\n",
+       "      <th>shadow</th>\n",
+       "      <th>is_rdf</th>\n",
+       "      <th>labels</th>\n",
+       "      <th>props</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>https://www.food.com/recipe/327593</td>\n",
+       "      <td>-1</td>\n",
+       "      <td>None</td>\n",
+       "      <td>None</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1</td>\n",
+       "      <td>True</td>\n",
+       "      <td>Recipe</td>\n",
+       "      <td>{\"minutes\":8,\"name\":\"anytime crepes\"}</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>https://www.food.com/recipe/327593</td>\n",
+       "      <td>0</td>\n",
+       "      <td>http://purl.org/heals/food/uses_ingredient</td>\n",
+       "      <td>http://purl.org/heals/ingredient/ChickenEgg</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1</td>\n",
+       "      <td>True</td>\n",
+       "      <td>None</td>\n",
+       "      <td></td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>https://www.food.com/recipe/327593</td>\n",
+       "      <td>1</td>\n",
+       "      <td>http://purl.org/heals/food/uses_ingredient</td>\n",
+       "      <td>http://purl.org/heals/ingredient/CowMilk</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1</td>\n",
+       "      <td>True</td>\n",
+       "      <td>None</td>\n",
+       "      <td></td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>https://www.food.com/recipe/327593</td>\n",
+       "      <td>2</td>\n",
+       "      <td>http://purl.org/heals/food/uses_ingredient</td>\n",
+       "      <td>http://purl.org/heals/ingredient/WholeWheatFlour</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1</td>\n",
+       "      <td>True</td>\n",
+       "      <td>None</td>\n",
+       "      <td></td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>https://www.food.com/recipe/327593</td>\n",
+       "      <td>3</td>\n",
+       "      <td>http://www.w3.org/1999/02/22-rdf-syntax-ns#type</td>\n",
+       "      <td>http://purl.org/heals/food/Recipe</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>-1</td>\n",
+       "      <td>True</td>\n",
+       "      <td>None</td>\n",
+       "      <td></td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                             src_name  edge_id  \\\n",
+       "0  https://www.food.com/recipe/327593       -1   \n",
+       "1  https://www.food.com/recipe/327593        0   \n",
+       "2  https://www.food.com/recipe/327593        1   \n",
+       "3  https://www.food.com/recipe/327593        2   \n",
+       "4  https://www.food.com/recipe/327593        3   \n",
+       "\n",
+       "                                          rel_name  \\\n",
+       "0                                             None   \n",
+       "1       http://purl.org/heals/food/uses_ingredient   \n",
+       "2       http://purl.org/heals/food/uses_ingredient   \n",
+       "3       http://purl.org/heals/food/uses_ingredient   \n",
+       "4  http://www.w3.org/1999/02/22-rdf-syntax-ns#type   \n",
+       "\n",
+       "                                           dst_name  truth  shadow  is_rdf  \\\n",
+       "0                                              None    1.0      -1    True   \n",
+       "1       http://purl.org/heals/ingredient/ChickenEgg    1.0      -1    True   \n",
+       "2          http://purl.org/heals/ingredient/CowMilk    1.0      -1    True   \n",
+       "3  http://purl.org/heals/ingredient/WholeWheatFlour    1.0      -1    True   \n",
+       "4                 http://purl.org/heals/food/Recipe    1.0      -1    True   \n",
+       "\n",
+       "   labels                                  props  \n",
+       "0  Recipe  {\"minutes\":8,\"name\":\"anytime crepes\"}  \n",
+       "1    None                                         \n",
+       "2    None                                         \n",
+       "3    None                                         \n",
+       "4    None                                         "
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "df = part.to_df()\n",
+    "df.head()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/pynock/pynock.py b/pynock/pynock.py
index 6e6a122..701f468 100644
--- a/pynock/pynock.py
+++ b/pynock/pynock.py
@@ -91,6 +91,11 @@ class Partition (BaseModel):  # pylint: disable=R0903
     """
 Representing a partition in the graph.
     """
+    SORT_COLUMNS: typing.ClassVar[typing.List[str]] = [
+        "src_name",
+        "edge_id",
+    ]
+
     part_id: IndexInts = NOT_FOUND  # type: ignore
     next_node: NonNegativeInt = 0
     nodes: typing.Dict[NonNegativeInt, Node] = {}
@@ -113,14 +118,14 @@ def lookup_node (
         return None
 
 
-    def create_node_name (
+    def _create_node_name (
         self,
         node_name: str,
         *,
         debug: bool = False,  # pylint: disable=W0613
         ) -> int:
         """
-Create a name for a new node in the namespace, looking up first to avoid duplicates.
+Private method to create a name for a new node in the namespace, looking up first to avoid duplicates.
         """
         node_id: IndexInts = NOT_FOUND  # type: ignore
 
@@ -136,6 +141,45 @@ def create_node_name (
         return node_id
 
 
+    def find_or_create_node (
+        self,
+        node_name: str,
+        *,
+        debug: bool = False,
+        ) -> Node:
+        """
+A utility method to:
+
+  * lookup a node by name and return if it already exists
+  * otherwise, create and return a new node
+
+Node attributes other than `node_id` and `name` can be set afterwards,
+as needed.
+        """
+        node: typing.Optional[Node] = self.lookup_node(
+            node_name,
+            debug = debug,
+        )
+
+        if node is None:
+            node_id: IndexInts = self._create_node_name(  # type: ignore
+                node_name,
+                debug = debug,
+            )
+
+            node = Node(
+                node_id = node_id,
+                name = node_name,
+            )
+
+            self.add_node(
+                node,
+                debug = debug,
+            )
+
+        return node
+
+
     @classmethod
     def _load_props (
         cls,
@@ -207,33 +251,28 @@ def _validation_error (
         )
 
 
-    def populate_node (
+    def _populate_node (
         self,
         row: GraphRow,
         *,
         debug: bool = False,  # pylint: disable=W0613
         ) -> Node:
         """
-Populate a Node object from the given Parquet row data.
-        """
-        # first, lookup to make sure we don't overwrite if there are
-        # duplicates for the src node
-        src_name: str = row["src_name"]
-        src_node: typing.Optional[Node] = self.lookup_node(src_name, debug=debug)
-
-        if src_node is None:
-            # create a src node
-            src_node = Node(
-                node_id = self.create_node_name(src_name, debug=debug),
-                name = row["src_name"],
-                truth = row["truth"],
-                is_rdf = row["is_rdf"],
-                shadow = row["shadow"],
-                label_set = set(row["labels"].split(",")),
-                prop_map = self._load_props(row["props"], debug=debug),
-            )
+Private method to populate a Node object from the given Parquet row data.
+        """
+        # lookup to make sure that we don't overwrite if the src node
+        # had any duplicate entries
+        src_node: Node = self.find_or_create_node(
+            row["src_name"],
+            debug = debug,
+        )
 
-            self.add_node(src_node, debug=debug)  # type: ignore
+        # in this case, we know these annotations must be added
+        src_node.truth = row["truth"]
+        src_node.is_rdf = row["is_rdf"]
+        src_node.shadow = row["shadow"]
+        src_node.label_set = set(row["labels"].split(","))
+        src_node.prop_map = self._load_props(row["props"], debug=debug)
 
         return src_node  # type: ignore
 
@@ -257,39 +296,58 @@ def get_edge_rel (
         return self.edge_rels.index(rel_name)
 
 
-    def populate_edge (
+    def create_edge (
+        self,
+        src_node: Node,
+        rel_name: str,
+        dst_node: Node,
+        *,
+        debug: bool = False,
+        ) -> Edge:
+        """
+Create an edge, which is effectively a triple
+        """
+        edge: Edge = Edge(
+            rel = self.get_edge_rel(rel_name, create=True, debug=debug),
+            node_id = dst_node.node_id,
+        )
+
+        src_node.add_edge(edge, debug=debug)
+
+        return edge
+
+
+    def _populate_edge (
         self,
         row: GraphRow,
-        node: Node,
+        src_node: Node,
         *,
         debug: bool = False,  # pylint: disable=W0613
         ) -> Edge:
         """
-Populate an Edge object from the given Parquet row data.
+Private method to populate an Edge object from the given Parquet row data.
         """
         # first, lookup the dst node and create if needed
-        dst_name: str = row["dst_name"]
-        dst_node: typing.Optional[Node] = self.lookup_node(dst_name, debug=debug)
-
-        if dst_node is None:
-            dst_node = Node(
-                node_id = self.create_node_name(dst_name, debug=debug),
-                name = dst_name,
-                truth = row["truth"],
-                is_rdf = row["is_rdf"],
-            )
+        dst_node: Node = self.find_or_create_node(
+            row["dst_name"],
+            debug = debug,
+        )
 
-            self.add_node(dst_node, debug=debug)
+        # add annotations
+        dst_node.truth = row["truth"]
+        dst_node.is_rdf = row["is_rdf"]
 
         # create the edge
-        edge: Edge = Edge(
-            rel = self.get_edge_rel(row["rel_name"], create=True, debug=debug),
-            truth = row["truth"],
-            node_id = dst_node.node_id,
-            prop_map = self._load_props(row["props"], debug=debug),
+        edge: Edge = self.create_edge(
+            src_node,
+            row["rel_name"],
+            dst_node,
+            debug = debug,
         )
 
-        node.add_edge(edge, debug=debug)
+        # add annotations
+        edge.truth = row["truth"]
+        edge.prop_map = self._load_props(row["props"], debug=debug)
 
         return edge
 
@@ -300,13 +358,24 @@ def dump_data (
         """
 Dump the internal data structures for this partition.
         """
-        for _, node_id in self.node_names.items():
-            node: Node = self.nodes[node_id]
-            ic(node)
+        for _, src_node_id in self.node_names.items():
+            src_node: Node = self.nodes[src_node_id]
+            self.dump_node(src_node)
 
-            for edge_rel, edge_list in node.edge_map.items():
-                for edge in edge_list:
-                    ic(edge_rel, edge)
+
+    def dump_node (
+        self,
+        node: Node,
+        ) -> None:
+        """
+Dump the internal data structures for this node.
+        """
+        ic(node)
+
+        for edge_rel, edge_list in node.edge_map.items():
+            for edge in edge_list:
+                dst_node: Node = self.nodes[edge.node_id]
+                ic(edge_rel, edge, dst_node.name)
 
 
     @classmethod
@@ -478,25 +547,25 @@ def parse_rows (
             # have we reached a row which begins a new node?
             if row["edge_id"] < 0:
                 try:
-                    node: Node = self.populate_node(row, debug=debug)
+                    src_node: Node = self._populate_node(row, debug=debug)
 
                     if debug:
                         print()
-                        ic(node)
+                        ic(src_node)
                 except ValidationError as ex:
                     self._validation_error(row_num, row, str(ex))
                     sys.exit(-1)
 
             # validate the node/edge sequencing and consistency among the rows
-            elif row["src_name"] != node.name:
+            elif row["src_name"] != src_node.name:
                 error_node = row["src_name"]
-                message = f"|{ error_node }| out of sequence at row {row_num}"
+                message = f"|{ error_node }| out of sequence at row { row_num }"
                 raise ValueError(message)
 
             # otherwise this row is an edge for the most recent node
             else:
                 try:
-                    edge: Edge = self.populate_edge(row, node, debug=debug)
+                    edge: Edge = self._populate_edge(row, src_node, debug=debug)
 
                     if debug:
                         ic(edge)
@@ -579,13 +648,15 @@ def to_df (
         """
 Represent the partition as a DataFrame.
         """
-        return pd.DataFrame([
+        df: pd.DataFrame = pd.DataFrame([
             row
-            for row in self.iter_gen_rows(
-                    sort = sort,
-                    debug = debug,
-            )
+            for row in self.iter_gen_rows(debug=debug)
         ])
+     
+        if sort:
+            df = df.sort_values(self.SORT_COLUMNS)
+
+        return df
 
 
     def save_file_parquet (
diff --git a/setup.py b/setup.py
index 7485c99..4621c9f 100644
--- a/setup.py
+++ b/setup.py
@@ -8,6 +8,8 @@
 import setuptools  # type: ignore
 
 
+VERSION = "1.2.0"
+
 DESCRIP = """
 A proposed standard `NOCK` for a Parquet format that supports efficient
 distributed serialization of multiple kinds of graph technologies.
@@ -20,10 +22,13 @@
     "dataframe",
     "graph data science",
     "knowledge graph",
+    "labeled property graphs",
+    "open standard",
     "openCypher",
+    "probabilistic graphs",
+    "semantic graphs",
     "serialization",
     "spreadsheet",
-    "open standard",
 ]
 
 
@@ -48,7 +53,7 @@ def parse_requirements_file (filename: str) -> typing.List[ str ]:
 if __name__ == "__main__":
     setuptools.setup(
         name = "pynock",
-        version = "1.1.1",
+        version = VERSION,
         license = "MIT",
 
         python_requires = ">=3.8",
diff --git a/tiny.py b/tiny.py
deleted file mode 100644
index ff0c1c4..0000000
--- a/tiny.py
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-A minimal example using `pynock` to construct a partition
-programmatically, based on the graph described in `dat/tiny.rdf`
-"""
-
-from icecream import ic
-import cloudpathlib
-
-from pynock import Partition, Node, Edge
-
-
-if __name__ == "__main__":
-    # initialize a partition
-    part: Partition = Partition(
-        part_id = 0,
-    )
-
-    # define a src node for the recipe
-    # NB: this node has properties, which RDF cannot query
-    src_name: str = "https://www.food.com/recipe/327593"
-
-    src_node: Node = Node(
-        node_id = part.create_node_name(src_name),
-        name = src_name,
-        is_rdf = True,
-        label_set = set(["Recipe"]),
-        prop_map = {
-            "minutes": 8,
-            "name": "anytime crepes"
-        },
-    )
-
-    part.add_node(src_node)
-
-    # define a dst node for the "Egg" ingredient
-    dst_name: str = "http://purl.org/heals/ingredient/ChickenEgg"
-
-    dst_node: Node = Node(
-        node_id = part.create_node_name(dst_name),
-        name = dst_name,
-        is_rdf = True,
-        label_set = set(["Ingredient"]),
-    )
-
-    part.add_node(dst_node)
-
-    # define an edge connecting src => dst for this ingredient
-    edge: Edge = Edge(
-        rel = part.get_edge_rel("http://purl.org/heals/food/uses_ingredient", create=True),
-        node_id = dst_node.node_id,
-    )
-
-    src_node.add_edge(edge)
-
-    # define a dst node for the "Milk" ingredient
-    dst_name = "http://purl.org/heals/ingredient/CowMilk"
-
-    dst_node = Node(
-        node_id = part.create_node_name(dst_name),
-        name = dst_name,
-        is_rdf = True,
-        label_set = set(["Ingredient"]),
-    )
-
-    part.add_node(dst_node)
-
-    # define an edge connecting src => dst for this ingredient
-    edge = Edge(
-        rel = part.get_edge_rel("http://purl.org/heals/food/uses_ingredient", create=True),
-        node_id = dst_node.node_id,
-    )
-
-    src_node.add_edge(edge)
-
-    # define a dst node for the "Flour" ingredient
-    # NB: this node has properties, which RDF cannot query
-    dst_name = "http://purl.org/heals/ingredient/WholeWheatFlour"
-
-    dst_node = Node(
-        node_id = part.create_node_name(dst_name),
-        name = dst_name,
-        is_rdf = True,
-        label_set = set(["Ingredient"]),
-        prop_map = {
-            "vegan": True,
-        },
-    )
-
-    part.add_node(dst_node)
-
-    # define an edge connecting src => dst for this ingredient
-    edge = Edge(
-        rel = part.get_edge_rel("http://purl.org/heals/food/uses_ingredient", create=True),
-        node_id = dst_node.node_id,
-    )
-
-    src_node.add_edge(edge)
-
-    # define a dst node for the "wtm:Recipe" parent
-    dst_name = "http://purl.org/heals/food/Recipe"
-
-    dst_node = Node(
-        node_id = part.create_node_name(dst_name),
-        name = dst_name,
-        is_rdf = True,
-        label_set = set(["top-level"]),
-    )
-
-    part.add_node(dst_node)
-
-    # define an edge connecting src => dst for this inheritance
-    edge = Edge(
-        rel = part.get_edge_rel("http://www.w3.org/1999/02/22-rdf-syntax-ns#type", create=True),
-        node_id = dst_node.node_id,
-    )
-
-    src_node.add_edge(edge)
-
-    # serialize this partition to multiple formats
-    part.save_file_parquet(cloudpathlib.AnyPath("foo.parq"))
-    part.save_file_csv(cloudpathlib.AnyPath("foo.csv"))
-    part.save_file_rdf(cloudpathlib.AnyPath("foo.rdf"), "ttl")
-
-    # check the files "foo.*" to see what was constructed programmatically
-    # also, here's a dataframe representation
-    df = part.to_df()
-    ic(df.head())
-


Test patch:
diff --git a/tests/test_pandas.py b/tests/test_pandas.py
index 5d65fc3..3025492 100644
--- a/tests/test_pandas.py
+++ b/tests/test_pandas.py
@@ -15,17 +15,21 @@
 import pandas as pd
 import pytest
 
+from pynock import Partition
+
 
 def test_pandas ():
     df_csv = pd.read_csv(
         cloudpathlib.AnyPath("dat/tiny.csv"),
-    ).fillna("")
+    ).fillna("").sort_values(Partition.SORT_COLUMNS).reset_index(drop=True)
+
     ic(df_csv.iloc[:, [2, 3, 7]])
 
     df_parq = pd.read_parquet(
         cloudpathlib.AnyPath("dat/tiny.parq"),
         use_nullable_dtypes = True,
-    ).fillna("")
+    ).fillna("").sort_values(Partition.SORT_COLUMNS).reset_index(drop=True)
+
     ic(df_parq.iloc[:, [2, 3, 7]])
 
     # general diff
diff --git a/tests/test_tiny.py b/tests/test_tiny.py
new file mode 100644
index 0000000..7f11c08
--- /dev/null
+++ b/tests/test_tiny.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Unit test coverage:
+
+  * construct a partition programmatically
+  * compare with a reference CSV file
+"""
+
+import tempfile
+
+from icecream import ic
+import cloudpathlib
+import pytest
+
+from pynock import Partition, Node, Edge
+
+
+def test_tiny ():
+    try:
+        load_csv: str = "dat/tiny.csv"
+        tmp_obs = tempfile.NamedTemporaryFile(mode="w+b", delete=True)
+
+        # construct a Partition
+        part: Partition = Partition(
+            part_id = 0,
+        )
+
+        # lookup/create the src node for the recipe
+        # NB: this node has properties, which RDF cannot query
+        src_name: str = "https://www.food.com/recipe/327593"
+        src_node: Node = part.find_or_create_node(src_name)
+
+        src_node.is_rdf = True
+        src_node.label_set = set(["Recipe"])
+        src_node.prop_map = {
+            "minutes": 8,
+            "name": "anytime crepes",
+        }
+
+        # lookup/create a dst node for the "Egg" ingredient
+        dst_name: str = "http://purl.org/heals/ingredient/ChickenEgg"
+        dst_node: Node = part.find_or_create_node(dst_name)
+
+        dst_node.is_rdf = True
+        dst_node.label_set = set(["Ingredient"])
+
+        # define an edge connecting src => dst for this ingredient
+        part.create_edge(
+            src_node,
+            "http://purl.org/heals/food/uses_ingredient",
+            dst_node,
+        )
+
+        # define a dst node for the "Milk" ingredient
+        dst_name = "http://purl.org/heals/ingredient/CowMilk"
+        dst_node = part.find_or_create_node(dst_name)
+
+        dst_node.is_rdf = True
+        dst_node.label_set = set(["Ingredient"])
+
+        # define an edge connecting src => dst for this ingredient
+        part.create_edge(
+            src_node,
+            "http://purl.org/heals/food/uses_ingredient",
+            dst_node,
+        )
+
+        # define a dst node for the "Flour" ingredient
+        # NB: this node has properties, which RDF cannot query
+        dst_name = "http://purl.org/heals/ingredient/WholeWheatFlour"
+        dst_node = part.find_or_create_node(dst_name)
+
+        dst_node.is_rdf = True
+        dst_node.label_set = set(["Ingredient"])
+        dst_node.prop_map = {
+            "vegan": True,
+        }
+
+        # define an edge connecting src => dst for this ingredient
+        part.create_edge(
+            src_node,
+            "http://purl.org/heals/food/uses_ingredient",
+            dst_node,
+        )
+
+        # define a dst node for the "wtm:Recipe" parent
+        dst_name = "http://purl.org/heals/food/Recipe"
+        dst_node = part.find_or_create_node(dst_name)
+
+        dst_node.is_rdf = True
+        dst_node.label_set = set(["top_level"])
+
+        # define an edge connecting src => dst for this inheritance
+        part.create_edge(
+            src_node,
+            "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
+            dst_node,
+        )
+
+        # write the partition as a CSV file
+        part.save_file_csv(
+            cloudpathlib.AnyPath(tmp_obs.name),
+            encoding = "utf-8",
+            sort = True,
+        )
+
+        # compare the respective texts
+        obs_text: str = cloudpathlib.AnyPath(tmp_obs.name).read_text()
+        exp_text: str = cloudpathlib.AnyPath(load_csv).read_text()
+
+        assert exp_text == obs_text
+
+    except Exception as ex:
+        ic(ex)
+
+    finally:
+        tmp_obs.close()