Repository: graspologic-org/graspologic-native (language: python)

Issue:
Infinite recursion when subgraph contains single large cluster
When a subgraph contains a cluster that is over the size threshold the hierarchical clustering code will try to recursively break up the cluster.  This is an infinite loop because the cluster will not break down any further.
So the relative path directories break maturin's build.  We needed to…
… build a new README anyway so this is it.


Discussion / hints:

Fix patch:
diff --git a/.devops/azure-pipeline.yml b/.devops/build.yml
similarity index 51%
rename from .devops/azure-pipeline.yml
rename to .devops/build.yml
index 0c3eeaa..cfcebf3 100644
--- a/.devops/azure-pipeline.yml
+++ b/.devops/build.yml
@@ -1,6 +1,6 @@
 jobs:
-- job: snapshot_build
-  displayName: Create Snapshot Build Number
+- job: build_identity
+  displayName: Materialize Python Build Number
   pool:
     vmImage: "ubuntu-latest"
   steps:
@@ -10,42 +10,16 @@ jobs:
     - script: |
         pip install -U pip
         pip install toml
-        python .devops/create_snapshot_version.py packages/pyo3/Cargo.toml "$BUILD_BUILDNUMBER"
-      displayName: "Create Version Number"
+        python .devops/manifest_version.py packages/pyo3/Cargo.toml "$BUILD_SOURCEBRANCHNAME" "$BUILD_BUILDNUMBER"
+      displayName: "Create Python Version Number"
     - publish: packages/pyo3/Cargo.toml
       artifact: cargotoml
-- job: rust_build
-  dependsOn: snapshot_build
-  displayName: Rust Build
-  pool:
-    vmImage: "ubuntu-latest"
-  steps:
-    - task: Rust@1
-      inputs:
-        installNightly: true
-    - script: |
-        cd packages/network_partitions
-        cargo test
-        cd ../packages/cli
-        cargo test
-        cargo build --release
-- job: cred_scan
-  dependsOn: rust_build
-  displayName: Cred Scan
-  pool:
-    vmImage: "windows-latest"
-  steps:
-    - task: CredScan@3
-      displayName: 'Run CredScan'
-      inputs:
-        outputFormat: sarif
-        debugMode: false
 - job: python_build
   displayName: Python Build
-  dependsOn: rust_build
+  dependsOn: build_identity
   strategy:
     matrix:
-      linux_p36: 
+      linux_p36:
         os: "ubuntu-latest"
         python_version: "3.6"
       linux_p37:
@@ -88,26 +62,6 @@ jobs:
         pip install maturin
         cd packages/pyo3
         cp $(Pipeline.Workspace)/cargotoml/Cargo.toml Cargo.toml
-        maturin build --release
+        python $(Build.SourcesDirectory)/.devops/homogenized_maturin_build.py $(python_version)
     - publish: target/wheels
       artifact: drop$(System.JobName)
-- job: Publish
-  dependsOn: python_build
-  pool:
-    vmImage: "ubuntu-latest"
-  steps:
-    - task: UsePythonVersion@0
-      inputs:
-        versionSpec: '3.8'
-    - download: current
-      patterns: '**/*.whl'
-    - script: |
-        pip install -U pip
-        pip install twine
-        twine upload --verbose --repository-url="https://nexus.ess-ex.net/repository/essex-pypi/" $(Pipeline.Workspace)/drop*/*.whl
-      displayName: "Upload BDists"
-      env: 
-        TWINE_USERNAME: $(ESSEXNEXUSUSERNAME)
-        TWINE_PASSWORD: $(ESSEXNEXUSPASSWORD)
-
-  
diff --git a/.devops/create_snapshot_version.py b/.devops/create_snapshot_version.py
deleted file mode 100644
index e1b56dc..0000000
--- a/.devops/create_snapshot_version.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import sys
-import toml
-
-if len(sys.argv) != 3:
-    print("Called with wrong arguments, requires: `python .devops/create_snapshot_version.py PATH BUILDID`")
-    exit(-1)
-
-cargo_conf = toml.load(sys.argv[1])
-build_date, build_id = sys.argv[2].split(".")
-original = cargo_conf["package"]["version"]
-snapshot = f"{original}-dev{build_date}{build_id.rjust(3,'0')}"
-cargo_conf["package"]["version"] = snapshot
-with open(sys.argv[1], "w") as cargo_file:
-    toml.dump(cargo_conf, cargo_file)
diff --git a/.devops/publish.yml b/.devops/publish.yml
new file mode 100644
index 0000000..d6870fe
--- /dev/null
+++ b/.devops/publish.yml
@@ -0,0 +1,25 @@
+jobs:
+  - template: build.yml
+  - job: inclusion_test
+    dependsOn: python_build
+    displayName: Internal PyPI
+    pool:
+      vmImage: "ubuntu-latest"
+    steps:
+      - download: current
+        patterns: '**/*.whl'
+        displayName: "Downloading all drops"
+      - task: UsePythonVersion@0
+        inputs:
+          versionSpec: '3.8'
+        displayName: 'Use Python 3.8'
+      - script: |
+          pip install twine
+        displayName: Install Twine
+      - task: TwineAuthenticate@1
+        inputs:
+          artifactFeed: essex-pypi
+        displayName: Authenticate Twine
+      - script: |
+          twine upload --skip-existing -r essex-pypi --config-file $(PYPIRC_PATH) $(Pipeline.Workspace)/drop*/*.whl --verbose 
+        displayName: Uploading to PyPI
diff --git a/.devops/sign.yml b/.devops/sign.yml
new file mode 100644
index 0000000..a45d9f5
--- /dev/null
+++ b/.devops/sign.yml
@@ -0,0 +1,63 @@
+jobs:
+  - template: build.yml
+  - job: signing_area
+    dependsOn: python_build
+    displayName: Signing Test Area
+    pool:
+      vmImage: "ubuntu-latest"
+    steps:
+      - download: current
+        patterns: '**/*.whl'
+      - script: |
+          set -x
+          mkdir wheels
+          find $(Pipeline.Workspace) -name *.whl -print0 | xargs -I % -0 mv -v % wheels/
+#          ls -ahl wheels
+#          cd wheels
+#          find . -name '*.whl' -print0 | xargs -I % -0 unzip -d %_unzipped %
+#          ls -ahlR
+        displayName: "Unpack and sign and stuff"
+      - task: EsrpCodeSigning@1
+        inputs:
+          ConnectedServiceName: 'ESRP Code Signing Service Connection'
+          FolderPath: 'wheels/'
+          Pattern: '*.whl'
+          signConfigType: 'inlineSignParams'
+          inlineOperation: |
+            [
+              {
+                "keyCode": "CP-231522",
+                "operationSetCode": "SigntoolSign",
+                "parameters": [
+                  {
+                    "parameterName": "OpusName",
+                    "parameterValue": "Microsoft"
+                  },
+                  {
+                    "parameterName": "OpusInfo",
+                    "parameterValue": "http://www.microsoft.com"
+                  },
+                  {
+                    "parameterName": "Append",
+                    "parameterValue" : "/as",
+                  },
+                  {
+                    "parameterName": "PageHash",
+                    "parameterValue": "/NPH"
+                  },
+                  {
+                    "parameterName": "FileDigest",
+                    "parameterValue": "/fd sha256"
+                  },
+                  {
+                    "parameterName": "TimeStamp",
+                    "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+                  }
+                ],
+                "toolName": "signtool.exe",
+                "toolVersion": "6.2.9304.0"
+              }
+            ]
+          SessionTimeout: '60'
+          MaxConcurrency: '50'
+          MaxRetryAttempts: '5'
diff --git a/.github/build/homogenized_maturin_build.py b/.github/build/homogenized_maturin_build.py
new file mode 100644
index 0000000..18de799
--- /dev/null
+++ b/.github/build/homogenized_maturin_build.py
@@ -0,0 +1,111 @@
+import sys
+import subprocess
+
+
+def execute() -> str:
+    maturin_list_interpreters = ["maturin", "list-python"]
+
+    results = subprocess.run(
+        maturin_list_interpreters,
+        encoding="utf8",
+        errors="ignore",
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE
+    )
+
+    if results.returncode != 0:
+        print(
+            f"Something happened when executing {' '.join(maturin_list_interpreters)}\n\tSTDERR: {results.stderr}\n\tSTDOUT: {results.stdout}",
+            file=sys.stderr
+        )
+        exit(-1)
+    return results.stdout
+
+
+def detect(
+    python_major_minor: str,
+    maturin_output: str
+) -> str:
+    search_sequence1 = f" - CPython {python_major_minor} at "
+    search_sequence2 = f" - CPython {python_major_minor}m at "
+    lines = maturin_output.split("\n")
+    matches = []
+    for line in lines:
+        if line.startswith(search_sequence1):
+            matches.append(line.replace(search_sequence1, "").strip())
+        elif line.startswith(search_sequence2):
+            matches.append(line.replace(search_sequence2, "").strip())
+
+    if len(matches) > 1:
+        print(f"We found more than one match meeting our major.minor criteria: {python_major_minor}", file=sys.stderr)
+        print(f"Interpreters found that match: {matches}")
+        exit(-1)
+    elif len(matches) == 0:
+        print(f"We were not able to extract any matches meeting our major.minor criteria: {python_major_minor} from {maturin_output}", file=sys.stderr)
+        exit(-1)
+    return matches[0]
+
+
+if __name__ == "__main__":
+    if len(sys.argv) != 2:
+        exit(-1)
+    interpreter = detect(sys.argv[1], execute())
+    results = subprocess.run(
+        ["maturin", "build", "--release", "-i", interpreter],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE
+    )
+
+    build_stdout = results.stdout.decode(
+        "cp1252",
+        errors="ignore"
+    )
+    build_stderr = results.stderr.decode(
+        "cp1252",
+        errors="ignore"
+    )
+    if results.returncode == 0:
+        print(build_stdout, file=sys.stdout)
+    else:
+        if results.stdout == results.stderr:
+            print(build_stderr, file=sys.stderr)
+        else:
+            print(f"STDOUT: {build_stdout}", file=sys.stdout)
+            print(f"STDERR: {build_stderr}", file=sys.stderr)
+    exit(results.returncode)
+
+
+# import unittest
+#
+#
+# class TestInterpreters(unittest.TestCase):
+#
+#     def test_windows(self):
+#         maturin_output_capture = """🐍 4 python interpreter found:
+#  - CPython 3.8 at C:\hostedtoolcache\windows\Python\3.8.5\x64\python.exe
+#  - CPython 3.7 at C:\hostedtoolcache\windows\Python\3.7.8\x64\python.exe
+#  - CPython 3.6 at C:\hostedtoolcache\windows\Python\3.6.8\x64\python.exe
+#  - CPython 3.5 at C:\hostedtoolcache\windows\Python\3.5.4\x64\python.exe
+# """
+#         expected = "C:\hostedtoolcache\windows\Python\3.8.5\x64\python.exe"
+#         results = detect("3.8", maturin_output_capture)
+#         self.assertEqual(expected, results)
+#
+#     def test_macos(self):
+#         maturin_output_capture = """🐍 1 python interpreter found:
+#  - CPython 3.8 at python3.8
+#
+# """
+#         expected = "python3.8"
+#         results = detect("3.8", maturin_output_capture)
+#         self.assertEqual(expected, results)
+#
+#     def test_linux(self):
+#         maturin_output_capture = """🐍 2 python interpreter found:
+#  - CPython 3.6m at python3.6
+#  - CPython 3.8 at python3.8
+#
+# """
+#         expected = "python3.8"
+#         results = detect("3.8", maturin_output_capture)
+#         self.assertEqual(expected, results)
diff --git a/.github/build/manifest_version.py b/.github/build/manifest_version.py
new file mode 100644
index 0000000..54cd265
--- /dev/null
+++ b/.github/build/manifest_version.py
@@ -0,0 +1,53 @@
+import sys
+import os
+import toml
+from datetime import datetime
+
+"""
+Github Env Variables:
+ - GITHUB_REF
+ - GITHUB_RUN_ID
+Devops Env Variables:
+ - BUILD_SOURCEBRANCH
+ - BUILD_BUILDNUMBER
+"""
+
+RELEASE_BRANCH = "refs/heads/main"
+
+BUILD_SYSTEM = "github"
+REF_KEY = "GITHUB_REF"
+BUILD_ID = "GITHUB_RUN_ID"
+
+if len(sys.argv) != 3:
+    print(f"Called with wrong arguments, requires: `python {sys.argv[0]} TOML_PATH VERSION_FILE_PATH")
+    sys.exit(-1)
+
+if REF_KEY not in os.environ:
+    print(f"{REF_KEY} not found in environment variable, adjust script for Azure Devops Pipelines or Github Actions")
+    sys.exit(-1)
+
+if BUILD_ID not in os.environ:
+    print(f"{BUILD_ID} not found in environment variable, adjust script for Azure Devops Pipelines or Github Actions")
+    sys.exit(-1)
+
+branch = os.environ[REF_KEY]
+build_id = os.environ[BUILD_ID]
+build_date = datetime.now().strftime("%Y%m%d")
+
+if BUILD_SYSTEM == "devops":
+    build_id = build_id.split(".")[1]
+_, cargo_file, version_file = sys.argv
+
+cargo_conf = toml.load(cargo_file)
+if branch != RELEASE_BRANCH:
+    # if it's a release branch, don't touch the TOML - it's fine
+    original = cargo_conf["package"]["version"]
+    snapshot = f"{original}-dev{build_date}{build_id.rjust(3,'0')}"
+    cargo_conf["package"]["version"] = snapshot
+    with open(cargo_file, "w") as cargo_file_io:
+        toml.dump(cargo_conf, cargo_file_io)
+
+version = cargo_conf["package"]["version"]
+
+with open(version_file, "w") as version_file_io:
+    version_file_io.write(version)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..05d213b
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,124 @@
+name: graspologic-native CI
+on: [push, pull_request]
+jobs:
+  materialize-build-version:
+    runs-on: "ubuntu-latest"
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python 3.8
+        uses: actions/setup-python@v1
+        with:
+          python-version: 3.8
+      - name: Materialize build number
+        run: |
+          pip install -U pip
+          pip install toml
+          python .github/build/manifest_version.py packages/pyo3/Cargo.toml version.txt
+      - uses: actions/upload-artifact@v2
+        with:
+          name: cargo-toml
+          path: packages/pyo3/Cargo.toml
+      - uses: actions/upload-artifact@v2
+        with:
+          name: version-txt
+          path: version.txt
+  code-check-rust:
+    runs-on: "ubuntu-latest"
+    steps:
+      - uses: actions/checkout@v2
+      - name: Check format
+        run: |
+          rustup toolchain install nightly --component rustfmt
+          rustup default nightly
+          cargo fmt -- --check
+  build-rust:
+    needs: ["materialize-build-version", "code-check-rust"]
+    runs-on: ${{ matrix.os }}
+    continue-on-error: true
+    strategy:
+      matrix:
+        os: ["ubuntu-latest", "windows-latest", "macos-latest"]
+        python_version: ["3.6", "3.7", "3.8", "3.9"]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{matrix.python_version}}
+        uses: actions/setup-python@v1
+        with:
+          python-version: ${{matrix.python_version}}
+      - uses: actions/download-artifact@v2
+        with:
+          name: cargo-toml
+          path: materialized
+      - name: "Materialized Cargo Placement"
+        run: |
+          mv packages/pyo3/Cargo.toml Cargo.toml.old
+          mv materialized/Cargo.toml packages/pyo3/Cargo.toml
+      - name: Update python environment
+        run: |
+          pip install -U pip setuptools wheel
+          pip install maturin
+      - name: Test
+        run: |
+          rustup default nightly
+          cargo test --manifest-path packages/network_partitions/Cargo.toml
+      - name: Package Python
+        run: |
+          cd packages/pyo3
+          python ${{github.workspace}}/.github/build/homogenized_maturin_build.py ${{matrix.python_version}}
+      - uses: actions/upload-artifact@v2
+        with:
+          name: wheels
+          path: target/wheels/*.whl
+  publish:
+    runs-on: ubuntu-latest
+    needs: "build-rust"
+    if: (github.ref=='refs/heads/main' || github.ref=='refs/heads/dev') && needs.build-rust.result=='success'
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python 3.8
+        uses: actions/setup-python@v1
+        with:
+          python-version: 3.8
+      - uses: actions/download-artifact@v2
+        with:
+          name: wheels
+          path: dist/
+      - name: Generate SHA256 files for each wheel
+        run: |
+          sha256sum dist/*.whl > checksums.txt
+          cat checksums.txt
+      - uses: actions/download-artifact@v2
+        with:
+          name: version-txt
+          path: version/
+      - name: Release Tag from Version
+        run: |
+          GRASPOLOGIC_VERSION=$(cat version/version.txt)
+          echo "GRASPOLOGIC_VERSION=$GRASPOLOGIC_VERSION" >> $GITHUB_ENV
+          echo $GRASPOLOGIC_VERSION
+      - name: Is Prerelease?
+        run: |
+          if [[ "$GITHUB_REF" == "ref/heads/dev" ]]; then
+            echo "GRASPOLOGIC_PRERELEASE=true" >> $GITHUB_ENV
+          else
+            echo "GRASPOLOGIC_PRERELEASE=false" >> $GITHUB_ENV
+          fi
+      - name: Install dependencies
+        run: |
+          python -m pip install -U pip setuptools wheel twine
+      - name: Publish with twine
+        env:
+          TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+          TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+        run: |
+          twine upload dist/*
+      - name: Create Github Release
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: ${{ env.GRASPOLOGIC_VERSION }}
+          release_name: graspologic-native-${{ env.GRASPOLOGIC_VERSION }}
+          body_path: "checksums.txt"
+          prerelease: ${{ env.GRASPOLOGIC_PRERELEASE }}
+
diff --git a/README.md b/README.md
index e3c9cef..0bbd111 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
-# topologic-native
-[topologic](https://github.com/microsoft/topologic) is an opinionated Python library built around a collection of the 
-best network/graph embedding techniques and other network related functionality best practices. 
+# graspologic-native
+
+[graspologic](https://github.com/microsoft/graspologic) is a python package for graph statistics. 
 
 Some functionality can be best served if compiled into a python native module, both for performance purposes and to 
 share that functionality with web assembly.
 
-`topologic-native` is a repository that holds Rust packages. The core packages will be published as crate libraries, 
+`graspologic-native` is a repository that holds Rust packages. The core packages will be published as crate libraries, 
 and a package using [pyo3](https://github.com/pyo3/pyo3) will expose the functionality of that library to Python.  
 
 ## Requirements
@@ -13,6 +13,9 @@ and a package using [pyo3](https://github.com/pyo3/pyo3) will expose the functio
 - Python 3.5+ (we are currently using 3.8)
 - 64 bit operating system
 
+## Published Versions
+We currently build for x86_64 platforms only, Windows, macOS, and Ubuntu, for python versions 3.6, 3.7, 3.8, and 3.9.
+
 ## Contributing
 
 This project welcomes contributions and suggestions. Most contributions require you to
@@ -30,6 +33,6 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi
 
 # Privacy
 
-`topologic-native` does not collect, store, or transmit any information of any kind back to Microsoft.
+`graspologic-native` does not collect, store, or transmit any information of any kind back to Microsoft.
 
 For your convenience, here is the link to the general [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement/). 
diff --git a/packages/cli/src/leiden.rs b/packages/cli/src/leiden.rs
index 5e9318a..f656af8 100644
--- a/packages/cli/src/leiden.rs
+++ b/packages/cli/src/leiden.rs
@@ -4,7 +4,7 @@
 use network_partitions::clustering::Clustering;
 use network_partitions::errors::CoreError;
 use network_partitions::leiden::leiden as leiden_internal;
-use network_partitions::network::{Network, NetworkBuilder};
+use network_partitions::network::prelude::*;
 use network_partitions::quality;
 
 use rand::SeedableRng;
@@ -29,21 +29,20 @@ pub fn leiden(
     skip_first_line: bool,
 ) {
     let start_instant: Instant = Instant::now();
-    let builder = NetworkBuilder::builder(use_modularity)
-        .load_from_file(
-            source_edges,
-            separator,
-            source_index,
-            target_index,
-            weight_index,
-            skip_first_line,
-        )
-        .expect("Something went wrong loading");
-    let loaded_file_instant: Instant = Instant::now();
+    let labeled_network: LabeledNetwork<String> = LabeledNetwork::<String>::load_from(
+        source_edges,
+        separator,
+        source_index,
+        target_index,
+        weight_index,
+        skip_first_line,
+        use_modularity,
+    )
+    .expect("Something went wrong loading");
 
-    let network: Network = builder.build();
+    let network: &CompactNetwork = labeled_network.compact();
 
-    let converted_network_instant: Instant = Instant::now();
+    let loaded_file_instant: Instant = Instant::now();
 
     let mut rng: XorShiftRng = match seed {
         Some(seed) => {
@@ -54,7 +53,7 @@ pub fn leiden(
     };
 
     let result: Result<(bool, Clustering), CoreError> = leiden_internal(
-        &network,
+        network,
         None,
         Some(iterations),
         Some(resolution),
@@ -68,22 +67,21 @@ pub fn leiden(
         Ok((improved, clustering)) => {
             println!("Clustering improved?  {}", improved);
             let quality_score: f64 =
-                quality::quality(&network, &clustering, Some(resolution), use_modularity)
+                quality::quality(network, &clustering, Some(resolution), use_modularity)
                     .expect("An error occurred when determining quality");
             println!("Quality score (modularity): {:?}", quality_score);
             println!("Output to {}", output_path);
 
             let mut output_file: File =
                 File::create(output_path).expect("Unable to open output file for writing");
-            for node_index in 0..clustering.num_nodes() {
-                let cluster: usize = clustering
-                    .cluster_at(node_index)
-                    .expect("Couldn't find a cluster for this node, which is impossible");
-                let node_name: String = network
-                    .node_name(node_index)
-                    .expect("Couldn't find the node name for this node");
-                write!(output_file, "{},{}\n", node_name, cluster)
-                    .expect("Could not entry to file");
+            for item in &clustering {
+                write!(
+                    output_file,
+                    "{},{}\n",
+                    labeled_network.label_for(item.node_id),
+                    item.cluster
+                )
+                .expect("Could not write entry to file");
             }
         }
         Err(err) => {
@@ -92,21 +90,13 @@ pub fn leiden(
     }
 
     let file_writer_instant: Instant = Instant::now();
-    println!(
-        "Time to initial load file: {:?}",
-        loaded_file_instant.duration_since(start_instant)
-    );
-    println!(
-        "Time to convert file: {:?}",
-        converted_network_instant.duration_since(loaded_file_instant)
-    );
     println!(
         "Time to load file: {:?}",
-        converted_network_instant.duration_since(start_instant)
+        loaded_file_instant.duration_since(start_instant)
     );
     println!(
         "Time to run leiden: {:?}",
-        leiden_completion_instant.duration_since(converted_network_instant)
+        leiden_completion_instant.duration_since(loaded_file_instant)
     );
     println!(
         "Time to output: {:?}",
diff --git a/packages/network_partitions/Cargo.toml b/packages/network_partitions/Cargo.toml
index 976d4f8..729a374 100644
--- a/packages/network_partitions/Cargo.toml
+++ b/packages/network_partitions/Cargo.toml
@@ -8,7 +8,6 @@ description = "Leiden community detection as per https://arxiv.org/abs/1810.0847
 
 [dependencies]
 rand = "0.7"
-rayon = "1.1"
 chrono = { version = "0.4", optional = true }
 
 [dev-dependencies]
@@ -16,3 +15,4 @@ rand_xorshift = "0.2"
 
 [features]
 logging = ["chrono"]
+debug = []
diff --git a/packages/network_partitions/src/clustering.rs b/packages/network_partitions/src/clustering.rs
index 46a8ba2..28d7095 100644
--- a/packages/network_partitions/src/clustering.rs
+++ b/packages/network_partitions/src/clustering.rs
@@ -3,7 +3,14 @@
 
 use super::errors::CoreError;
 use super::safe_vectors::SafeVectors;
+use crate::network::{CompactNodeId, LabeledNetwork};
 use std::collections::HashMap;
+use std::ops::Index;
+
+pub struct ClusterItem {
+    pub node_id: usize,
+    pub cluster: usize,
+}
 
 /// Clustering is not a great abstraction; the details of it are purposefully spilled to the
 /// clustering algorithm for optimal computational runtime, but it's important to note that the
@@ -86,8 +93,8 @@ impl Clustering {
 
     /// Generates a vector of nodes for each cluster with the index referencing the cluster and the
     /// value being a count from 0 upward.
-    pub fn num_nodes_per_cluster(&self) -> Vec<usize> {
-        let mut nodes_per_cluster: Vec<usize> = vec![0 as usize; self.next_cluster_id];
+    pub fn num_nodes_per_cluster(&self) -> Vec<u64> {
+        let mut nodes_per_cluster: Vec<u64> = vec![0 as u64; self.next_cluster_id];
         for i in 0..self.node_to_cluster_mapping.len() {
             nodes_per_cluster[self.node_to_cluster_mapping[i]] += 1;
         }
@@ -96,17 +103,17 @@ impl Clustering {
 
     /// Generates a vector containing every node id for every cluster id. The outer vector index
     /// corresponds to the cluster id, and the values in the inner vectors correspond to the node ids.
-    pub fn nodes_per_cluster(&self) -> Vec<Vec<usize>> {
-        let number_nodes_per_cluster: Vec<usize> = self.num_nodes_per_cluster();
-        let mut nodes_per_cluster_vec: Vec<Vec<usize>> = Vec::with_capacity(self.next_cluster_id);
+    pub fn nodes_per_cluster(&self) -> Vec<Vec<CompactNodeId>> {
+        let number_nodes_per_cluster: Vec<u64> = self.num_nodes_per_cluster();
+        let mut nodes_per_cluster: Vec<Vec<CompactNodeId>> =
+            Vec::with_capacity(self.next_cluster_id);
         for i in 0..self.next_cluster_id {
-            nodes_per_cluster_vec.push(Vec::with_capacity(number_nodes_per_cluster[i]));
+            nodes_per_cluster.push(Vec::with_capacity(number_nodes_per_cluster[i] as usize));
         }
-        for i in 0..self.node_to_cluster_mapping.len() {
-            let cluster: usize = self.node_to_cluster_mapping[i];
-            nodes_per_cluster_vec[cluster].push(i);
+        for (node_id, cluster) in self.node_to_cluster_mapping.iter().enumerate() {
+            nodes_per_cluster[*cluster].push(node_id);
         }
-        return nodes_per_cluster_vec;
+        return nodes_per_cluster;
     }
 
     /// This method compacts the Clustering, removing empty clusters and applying new cluster IDs
@@ -141,49 +148,18 @@ impl Clustering {
         }
     }
 
-    /// Orders the clusters in our Clustering by ensuring the clusters with the largest population
-    /// (e.g. the most nodes) are at the front of the node_to_cluster_mapping array.
-    /// Note that this function also drops empty clusters.
-    pub fn order_clusters_by_population(&mut self) {
-        let mut cluster_weights: Vec<u32> = vec![0u32; self.next_cluster_id];
-        for i in 0..self.node_to_cluster_mapping.len() {
-            cluster_weights[self.node_to_cluster_mapping[i]] += 1u32;
-        }
-        let mut weighted_clusters: Vec<(usize, &u32)> =
-            cluster_weights.iter().enumerate().collect();
-        weighted_clusters.sort_by(|a, b| a.1.cmp(b.1).reverse());
-        let mut new_cluster_index: usize = 0;
-        let mut old_cluster_to_new_cluster_map: Vec<usize> = vec![0; self.next_cluster_id];
-        loop {
-            old_cluster_to_new_cluster_map[weighted_clusters[new_cluster_index].0] =
-                new_cluster_index;
-            new_cluster_index += 1;
-            if new_cluster_index >= self.next_cluster_id
-                || *weighted_clusters[new_cluster_index].1 == 0
-            {
-                break;
-            }
-        }
-        self.next_cluster_id = new_cluster_index;
-        for i in 0..self.node_to_cluster_mapping.len() {
-            self.node_to_cluster_mapping[i] =
-                old_cluster_to_new_cluster_map[self.node_to_cluster_mapping[i]];
-        }
-    }
-
     pub fn reset_next_cluster_id(&mut self) {
         self.next_cluster_id = 0;
     }
 
     pub fn merge_subnetwork_clustering(
         &mut self,
-        subnetwork_nodes: &Vec<usize>,
+        subnetwork: &LabeledNetwork<CompactNodeId>,
         subnetwork_clustering: &Clustering,
     ) {
-        for subnetwork_node_index in 0..subnetwork_nodes.len() {
-            let network_node_index: usize = subnetwork_nodes[subnetwork_node_index];
-            self.node_to_cluster_mapping[network_node_index] = self.next_cluster_id
-                + subnetwork_clustering.node_to_cluster_mapping[subnetwork_node_index];
+        for (new_id, old_id) in subnetwork.labeled_ids() {
+            self.node_to_cluster_mapping[*old_id] =
+                self.next_cluster_id + subnetwork_clustering.node_to_cluster_mapping[new_id];
         }
         self.next_cluster_id += subnetwork_clustering.next_cluster_id;
     }
@@ -214,6 +190,51 @@ impl From<Clustering> for HashMap<usize, usize> {
     }
 }
 
+pub struct ClusterIterator<'a> {
+    cluster_ref: &'a Clustering,
+    next_cluster_id: usize,
+}
+
+impl<'a> Iterator for ClusterIterator<'a> {
+    type Item = ClusterItem;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        return if self.next_cluster_id == self.cluster_ref.node_to_cluster_mapping.len() {
+            None
+        } else {
+            let item = ClusterItem {
+                node_id: self.next_cluster_id,
+                cluster: self.cluster_ref[self.next_cluster_id],
+            };
+            self.next_cluster_id += 1;
+            Some(item)
+        };
+    }
+}
+
+impl<'a> IntoIterator for &'a Clustering {
+    type Item = ClusterItem;
+    type IntoIter = ClusterIterator<'a>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        return ClusterIterator {
+            cluster_ref: &self,
+            next_cluster_id: 0,
+        };
+    }
+}
+
+impl Index<usize> for Clustering {
+    type Output = usize;
+
+    fn index(
+        &self,
+        index: usize,
+    ) -> &Self::Output {
+        &self.node_to_cluster_mapping[index]
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::Clustering;
@@ -243,20 +264,6 @@ mod tests {
         assert_eq!(clustering, expected);
     }
 
-    #[test]
-    pub fn test_order_clusters_by_population() {
-        let mut clustering: Clustering = Clustering {
-            node_to_cluster_mapping: vec![3, 3, 4, 1, 2, 2, 5, 0],
-            next_cluster_id: 6,
-        };
-        let expected: Clustering = Clustering {
-            node_to_cluster_mapping: vec![1, 1, 4, 3, 0, 0, 5, 2],
-            next_cluster_id: 6,
-        };
-        clustering.order_clusters_by_population();
-        assert_eq!(clustering, expected);
-    }
-
     #[test]
     pub fn test_merge_clusters() {
         let mut clustering: Clustering = Clustering {
@@ -281,13 +288,13 @@ mod tests {
             node_to_cluster_mapping: vec![1, 1, 4, 3, 0, 0, 5, 2],
             next_cluster_id: 6,
         };
-        let expected: Vec<usize> = vec![2, 2, 1, 1, 1, 1];
+        let expected: Vec<u64> = vec![2, 2, 1, 1, 1, 1];
         assert_eq!(expected, clustering.num_nodes_per_cluster());
         let clustering: Clustering = Clustering {
             node_to_cluster_mapping: vec![],
             next_cluster_id: 0,
         };
-        let expected: Vec<usize> = Vec::new();
+        let expected: Vec<u64> = Vec::new();
         assert_eq!(expected, clustering.num_nodes_per_cluster());
     }
 
diff --git a/packages/network_partitions/src/leiden/full_network_clustering.rs b/packages/network_partitions/src/leiden/full_network_clustering.rs
index eadd726..f267f04 100644
--- a/packages/network_partitions/src/leiden/full_network_clustering.rs
+++ b/packages/network_partitions/src/leiden/full_network_clustering.rs
@@ -7,12 +7,12 @@ use crate::clustering::Clustering;
 use crate::errors::CoreError;
 use crate::leiden::neighboring_clusters::NeighboringClusters;
 use crate::log;
-use crate::network::Network;
+use crate::network::prelude::*;
 use crate::progress_meter;
 use rand::Rng;
 
 pub fn full_network_clustering<T>(
-    network: &Network,
+    network: &CompactNetwork,
     clustering: &mut Clustering,
     adjusted_resolution: f64,
     rng: &mut T,
@@ -54,7 +54,7 @@ where
 
         let current_node: usize = work_queue.pop_front()?;
         let current_cluster: usize = clustering.cluster_at(current_node)?;
-        let current_node_weight: f64 = network.node_weight_at(current_node)?;
+        let current_node_weight: f64 = network.node_weight(current_node);
 
         // temporarily presume we're not in any cluster (we'll add this back later after we've
         // decided on the appropriate cluster to belong to)
@@ -134,22 +134,22 @@ where
 }
 
 fn weights_and_counts_per_cluster(
-    network: &Network,
+    network: &CompactNetwork,
     clustering: &Clustering,
 ) -> Result<(Vec<f64>, Vec<usize>), CoreError> {
     let mut cluster_weights: Vec<f64> = vec![0_f64; network.num_nodes()];
     let mut num_nodes_per_cluster: Vec<usize> = vec![0; network.num_nodes()];
 
-    for node in 0..network.num_nodes() {
-        let cluster_id: usize = clustering.cluster_at(node)?;
-        cluster_weights[cluster_id] += network.node_weight_at(node)?;
+    for compact_node in network {
+        let cluster_id: usize = clustering.cluster_at(compact_node.id)?;
+        cluster_weights[cluster_id] += compact_node.weight;
         num_nodes_per_cluster[cluster_id] += 1;
     }
     return Ok((cluster_weights, num_nodes_per_cluster));
 }
 
 fn unused_clusters(
-    network: &Network,
+    network: &CompactNetwork,
     num_nodes_per_cluster: &Vec<usize>,
 ) -> (Vec<usize>, usize) {
     let size: usize = network.num_nodes() - 1;
@@ -184,7 +184,7 @@ fn leave_current_cluster(
 }
 
 fn identify_neighboring_clusters(
-    network: &Network,
+    network: &CompactNetwork,
     clustering: &Clustering,
     current_node: usize,
     current_cluster: usize,
@@ -196,12 +196,9 @@ fn identify_neighboring_clusters(
     let next_unused_cluster: usize = unused_clusters[num_unused_clusters - 1];
     neighboring_clusters.increase_cluster_weight(next_unused_cluster, 0_f64);
 
-    let (start_neighbor_index, end_neighbor_index) = network.neighbor_range(current_node)?;
-
-    for neighbor_index in start_neighbor_index..end_neighbor_index {
-        let (neighbor_node, weight) = network.edge_at(neighbor_index)?;
-        let neighbor_cluster: usize = clustering.cluster_at(neighbor_node)?;
-        neighboring_clusters.increase_cluster_weight(neighbor_cluster, weight);
+    for neighbor in network.neighbors_for(current_node) {
+        let neighbor_cluster: usize = clustering.cluster_at(neighbor.id)?;
+        neighboring_clusters.increase_cluster_weight(neighbor_cluster, neighbor.edge_weight);
     }
     neighboring_clusters.freeze();
     return Ok(());
@@ -255,17 +252,15 @@ fn join_cluster(
 }
 
 fn trigger_cluster_change(
-    network: &Network,
+    network: &CompactNetwork,
     clustering: &Clustering,
     work_queue: &mut FullNetworkWorkQueue,
     node: usize,
     best_cluster: usize,
 ) -> Result<(), CoreError> {
-    let (start_neighbor_index, end_neighbor_index) = network.neighbor_range(node)?;
-    for neighbor_index in start_neighbor_index..end_neighbor_index {
-        let neighbor: usize = network.neighbor_at(neighbor_index)?;
-        if clustering.cluster_at(neighbor)? != best_cluster {
-            work_queue.push_back(neighbor);
+    for neighbor in network.neighbors_for(node) {
+        if clustering.cluster_at(neighbor.id)? != best_cluster {
+            work_queue.push_back(neighbor.id);
         }
     }
     return Ok(());
@@ -274,7 +269,7 @@ fn trigger_cluster_change(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::network::NetworkBuilder;
+    use crate::network::{Edge, LabeledNetwork};
     use crate::resolution;
     use rand::SeedableRng;
     use rand_xorshift::XorShiftRng;
@@ -286,70 +281,79 @@ mod tests {
         // generate same graph as in java, done via Network object not InternalNetwork, then
         // generate a InternalNetwork from it
         // we should expect 3 clusters, 2 with a light connection and 1 with no connection
-        let network: Network = NetworkBuilder::for_modularity()
-            .add_edge_into("dwayne", "nick", 15.0)
-            .add_edge_into("nick", "jon", 12.0)
-            .add_edge_into("jon", "carolyn", 14.0)
-            .add_edge_into("nick", "carolyn", 10.0)
-            .add_edge_into("dwayne", "jon", 12.0)
-            .add_edge_into("carolyn", "amber", 1.0)
-            .add_edge_into("amber", "chris", 12.0)
-            .add_edge_into("amber", "nathan", 15.0)
-            .add_edge_into("nathan", "chris", 5.0)
-            .add_edge_into("jarkko", "thirteen", 15.0)
-            .build();
-
-        let mut clustering: Clustering = Clustering::as_self_clusters(network.num_nodes());
-
-        let adjusted_resolution: f64 = resolution::adjust_resolution(Option::None, &network, true);
-
-        let improved =
-            full_network_clustering(&network, &mut clustering, adjusted_resolution, &mut rng)
-                .unwrap();
+
+        let edges: Vec<Edge> = vec![
+            ("dwayne".into(), "nick".into(), 15.0),
+            ("nick".into(), "jon".into(), 15.0),
+            ("jon".into(), "carolyn".into(), 15.0),
+            ("nick".into(), "carolyn".into(), 15.0),
+            ("dwayne".into(), "jon".into(), 15.0),
+            ("carolyn".into(), "amber".into(), 15.0),
+            ("amber".into(), "chris".into(), 15.0),
+            ("amber".into(), "nathan".into(), 15.0),
+            ("nathan".into(), "chris".into(), 15.0),
+            ("jarkko".into(), "thirteen".into(), 15.0),
+        ];
+
+        let mut builder: LabeledNetworkBuilder<String> = LabeledNetworkBuilder::new();
+        let labeled_network: LabeledNetwork<String> = builder.build(edges.into_iter(), true);
+
+        let mut clustering: Clustering = Clustering::as_self_clusters(labeled_network.num_nodes());
+
+        let adjusted_resolution: f64 =
+            resolution::adjust_resolution(Option::None, labeled_network.compact(), true);
+
+        let improved = full_network_clustering(
+            labeled_network.compact(),
+            &mut clustering,
+            adjusted_resolution,
+            &mut rng,
+        )
+        .unwrap();
 
         assert!(improved);
         let nathan_cluster: usize = clustering
-            .cluster_at(network.index_for_name("nathan").unwrap().clone())
+            .cluster_at(labeled_network.compact_id_for("nathan".into()).unwrap())
             .unwrap();
         let dwayne_cluster: usize = clustering
-            .cluster_at(network.index_for_name("dwayne").unwrap().clone())
+            .cluster_at(labeled_network.compact_id_for("dwayne".into()).unwrap())
             .unwrap();
         let jarkko_cluster: usize = clustering
-            .cluster_at(network.index_for_name("jarkko").unwrap().clone())
+            .cluster_at(labeled_network.compact_id_for("jarkko".into()).unwrap())
             .unwrap();
 
         assert_eq!(
             nathan_cluster,
             clustering
-                .cluster_at(network.index_for_name("chris").unwrap().clone())
+                .cluster_at(labeled_network.compact_id_for("chris".into()).unwrap())
                 .unwrap(),
             "Expected chris in nathan cluster"
         );
         assert_eq!(
             nathan_cluster,
             clustering
-                .cluster_at(network.index_for_name("amber").unwrap().clone())
+                .cluster_at(labeled_network.compact_id_for("amber".into()).unwrap())
                 .unwrap(),
             "Expected amber in nathan cluster"
         );
         assert_eq!(
             dwayne_cluster,
             clustering
-                .cluster_at(network.index_for_name("jon").unwrap().clone())
+                .cluster_at(labeled_network.compact_id_for("jon".into()).unwrap())
                 .unwrap(),
             "Expected jon in dwayne cluster"
         );
         assert_eq!(
             dwayne_cluster,
             clustering
-                .cluster_at(network.index_for_name("nick").unwrap().clone())
+                .cluster_at(labeled_network.compact_id_for("nick".into()).unwrap())
                 .unwrap(),
             "Expected nick in dwayne cluster"
         );
         assert_eq!(
             dwayne_cluster,
             clustering
-                .cluster_at(network.index_for_name("carolyn").unwrap().clone())
+                .cluster_at(labeled_network.compact_id_for("carolyn".into()).unwrap())
                 .unwrap(),
             "Expected carolyn in dwayne cluster"
         );
diff --git a/packages/network_partitions/src/leiden/hierarchical.rs b/packages/network_partitions/src/leiden/hierarchical.rs
new file mode 100644
index 0000000..d81e12f
--- /dev/null
+++ b/packages/network_partitions/src/leiden/hierarchical.rs
@@ -0,0 +1,234 @@
+use rand::Rng;
+
+use super::leiden::leiden;
+use crate::clustering::{ClusterItem, Clustering};
+use crate::errors::CoreError;
+use crate::log;
+use crate::network::prelude::*;
+use std::collections::HashSet;
+use std::collections::VecDeque;
+
+struct HierarchicalWork {
+    subnetwork: LabeledNetwork<CompactNodeId>,
+    parent_cluster: ClusterId,
+}
+
+pub struct HierarchicalCluster {
+    pub node: CompactNodeId,
+    pub cluster: ClusterId,
+    pub level: u32,
+    pub parent_cluster: Option<ClusterId>,
+    pub is_final_cluster: bool,
+}
+
+struct HierarchicalClustering {
+    hierarchical_clusterings: Vec<HierarchicalCluster>,
+    cluster_range: Vec<(usize, usize)>,
+}
+
+impl HierarchicalClustering {
+    pub fn new(initial_clustering: &Clustering) -> Self {
+        let mut cluster_range: Vec<(usize, usize)> =
+            Vec::with_capacity(initial_clustering.next_cluster_id());
+        let mut hierarchical: Vec<HierarchicalCluster> =
+            Vec::with_capacity(initial_clustering.num_nodes());
+
+        let ordered_cluster_items: Vec<ClusterItem> = initial_clustering.sorted_cluster_items();
+
+        let mut range_start: usize = 0;
+        let mut previous_cluster: usize = 0;
+        for cluster_item in ordered_cluster_items {
+            let hierarchical_cluster = HierarchicalCluster {
+                node: cluster_item.node_id,
+                cluster: cluster_item.cluster,
+                level: 0,
+                parent_cluster: None,
+                is_final_cluster: true,
+            };
+            if cluster_item.cluster != previous_cluster {
+                cluster_range.push((range_start, hierarchical.len()));
+                range_start = hierarchical.len();
+            }
+            hierarchical.push(hierarchical_cluster);
+            previous_cluster = cluster_item.cluster;
+        }
+        cluster_range.push((range_start, hierarchical.len()));
+
+        return HierarchicalClustering {
+            hierarchical_clusterings: hierarchical,
+            cluster_range,
+        };
+    }
+
+    pub fn insert_subnetwork_clustering(
+        &mut self,
+        subnetwork: &LabeledNetwork<CompactNodeId>,
+        subnetwork_clustering: &Clustering,
+        parent_cluster: ClusterId,
+        starting_cluster_id: ClusterId,
+        level: u32,
+    ) -> usize {
+        let ordered_cluster_items: Vec<ClusterItem> = subnetwork_clustering.sorted_cluster_items();
+
+        let mut range_start: usize = self.hierarchical_clusterings.len();
+        let mut iter_cluster_prev: ClusterId = 0;
+        let mut final_cluster_id: usize = 0;
+
+        for cluster_item in ordered_cluster_items {
+            final_cluster_id = cluster_item.cluster + 1;
+            let hierarchical_cluster = HierarchicalCluster {
+                node: *subnetwork.label_for(cluster_item.node_id),
+                cluster: cluster_item.cluster + starting_cluster_id,
+                level,
+                parent_cluster: Some(parent_cluster),
+                is_final_cluster: true,
+            };
+            if cluster_item.cluster != iter_cluster_prev {
+                self.cluster_range
+                    .push((range_start, self.hierarchical_clusterings.len()));
+                range_start = self.hierarchical_clusterings.len();
+            }
+            self.hierarchical_clusterings.push(hierarchical_cluster);
+            iter_cluster_prev = cluster_item.cluster;
+        }
+        self.cluster_range
+            .push((range_start, self.hierarchical_clusterings.len()));
+
+        let (start, end) = self.cluster_range[parent_cluster];
+
+        for old_hierarchical_cluster_entry in start..end {
+            self.hierarchical_clusterings[old_hierarchical_cluster_entry].is_final_cluster = false;
+        }
+
+        return final_cluster_id;
+    }
+}
+
+trait OrderedClustering {
+    fn sorted_cluster_items(&self) -> Vec<ClusterItem>;
+}
+
+impl OrderedClustering for Clustering {
+    fn sorted_cluster_items(&self) -> Vec<ClusterItem> {
+        let mut ordered_cluster_items: Vec<ClusterItem> = self.into_iter().collect();
+        ordered_cluster_items
+            .sort_by(|a, b| a.cluster.cmp(&b.cluster).then(a.node_id.cmp(&b.node_id)));
+        return ordered_cluster_items;
+    }
+}
+
+pub fn hierarchical_leiden<T>(
+    network: &CompactNetwork,
+    clustering: Option<Clustering>,
+    iterations: Option<usize>,
+    resolution: Option<f64>,
+    randomness: Option<f64>,
+    rng: &mut T,
+    use_modularity: bool,
+    max_cluster_size: u32,
+) -> Result<Vec<HierarchicalCluster>, CoreError>
+where
+    T: Rng + Clone + Send,
+{
+    let (_, mut updated_clustering) = leiden(
+        network,
+        clustering,
+        iterations,
+        resolution,
+        randomness,
+        rng,
+        use_modularity,
+    )?;
+
+    log!("First clustering completed.");
+
+    let mut hierarchical_clustering: HierarchicalClustering =
+        HierarchicalClustering::new(&updated_clustering);
+    let mut work_queue: VecDeque<HierarchicalWork> = VecDeque::new();
+    let mut level: u32 = 1;
+    let mut clusters_that_did_not_split: HashSet<usize> = HashSet::new();
+
+    let nodes_by_cluster: Vec<Vec<CompactNodeId>> = updated_clustering.nodes_per_cluster();
+
+    for subnetwork in network.filtered_subnetworks(
+        &updated_clustering,
+        &nodes_by_cluster,
+        max_cluster_size,
+        use_modularity,
+    ) {
+        log!(
+            "Cluster {} contains more than {} values and will be added to the work queue",
+            subnetwork.id,
+            max_cluster_size
+        );
+        work_queue.push_back(HierarchicalWork {
+            subnetwork: subnetwork.subnetwork,
+            parent_cluster: subnetwork.id,
+        });
+    }
+
+    while !work_queue.is_empty() {
+        let work_item: HierarchicalWork = work_queue.pop_front().unwrap();
+        let subnetwork: LabeledNetwork<CompactNodeId> = work_item.subnetwork;
+        let (_, subnetwork_clustering) = leiden(
+            subnetwork.compact(),
+            None,
+            iterations,
+            resolution,
+            randomness,
+            rng,
+            use_modularity,
+        )?;
+        let offset: usize = updated_clustering.next_cluster_id();
+
+        if subnetwork_clustering.next_cluster_id() == 1 {
+            // we couldn't break this cluster down any further.
+            clusters_that_did_not_split.insert(work_item.parent_cluster);
+            log!(
+                "Cluster {} did not split so we will not re-process it.",
+                work_item.parent_cluster
+            );
+        } else {
+            hierarchical_clustering.insert_subnetwork_clustering(
+                &subnetwork,
+                &subnetwork_clustering,
+                work_item.parent_cluster,
+                offset,
+                level,
+            );
+            for clustering_item in &subnetwork_clustering {
+                let new_cluster_id: ClusterId = clustering_item.cluster + offset;
+                let old_node_id: CompactNodeId = *subnetwork.label_for(clustering_item.node_id);
+                updated_clustering.update_cluster_at(old_node_id, new_cluster_id)?;
+            }
+        }
+
+        if work_queue.is_empty() {
+            log!("Level {} complete, seeking any other clusters larger than {} size for further refinement", level, max_cluster_size);
+            level += 1;
+            let nodes_by_cluster: Vec<Vec<CompactNodeId>> = updated_clustering.nodes_per_cluster();
+            for subnetwork in network.subnetworks_iter(
+                &updated_clustering,
+                &nodes_by_cluster,
+                Some(max_cluster_size),
+            ) {
+                // Push the current subgraph onto the work_queue if-and-only-if the subgraph
+                // has more than one node AND the parent graph has more than one subgraph.
+                if nodes_by_cluster[subnetwork.id].len() > 1
+                    && !clusters_that_did_not_split.contains(&subnetwork.id)
+                {
+                    work_queue.push_back(HierarchicalWork {
+                        subnetwork: subnetwork.subnetwork,
+                        parent_cluster: subnetwork.id,
+                    });
+                }
+            }
+        }
+    }
+    log!(
+        "Unable to break down {} clusters, {:?}",
+        clusters_that_did_not_split.len(),
+        clusters_that_did_not_split
+    );
+    return Ok(hierarchical_clustering.hierarchical_clusterings);
+}
diff --git a/packages/network_partitions/src/leiden/leiden.rs b/packages/network_partitions/src/leiden/leiden.rs
index 9bd9670..31cef05 100644
--- a/packages/network_partitions/src/leiden/leiden.rs
+++ b/packages/network_partitions/src/leiden/leiden.rs
@@ -8,7 +8,7 @@ use rand::Rng;
 use crate::clustering::Clustering;
 use crate::errors::CoreError;
 use crate::log;
-use crate::network::Network;
+use crate::network::prelude::*;
 use crate::progress_meter;
 use crate::resolution::adjust_resolution;
 
@@ -57,7 +57,7 @@ const DEFAULT_ITERATIONS: usize = 1;
 ///   InternalNetwork::for_cpm_maximization and ensure you use the function that builds the corresponding
 ///   InternalNetwork for this setting.
 pub fn leiden<T>(
-    network: &Network,
+    network: &CompactNetwork,
     clustering: Option<Clustering>,
     iterations: Option<usize>,
     resolution: Option<f64>,
@@ -96,6 +96,7 @@ where
         improved |= improve_clustering(
             network,
             &mut clustering,
+            use_modularity,
             adjusted_resolution,
             randomness,
             rng,
@@ -107,8 +108,9 @@ where
 
 /// This function will be executed repeatedly as per number_iterations
 fn improve_clustering<T>(
-    network: &Network,
+    network: &CompactNetwork,
     clustering: &mut Clustering,
+    use_modularity: bool,
     adjusted_resolution: f64,
     randomness: f64,
     rng: &mut T,
@@ -134,21 +136,53 @@ where
         // given the updated clustering, generate subnetworks for each cluster comprised solely of the
         // nodes in that cluster, then fast, low-fidelity cluster the subnetworks, merging the results
         // back into the primary clustering before returning
-        let subnetworks: Vec<Network> = network.subnetworks_for_clustering(clustering)?;
-        let nodes_per_cluster: Vec<Vec<usize>> = clustering.nodes_per_cluster();
-        clustering.reset_next_cluster_id();
+        let nodes_by_cluster: Vec<Vec<CompactNodeId>> = clustering.nodes_per_cluster();
+        let subnetworks_iterator = network.subnetworks_iter(clustering, &nodes_by_cluster, None);
+        let num_nodes_per_cluster: Vec<u64> = clustering.num_nodes_per_cluster();
+
+        let num_subnetworks: usize = clustering.next_cluster_id();
 
-        let num_nodes_per_cluster_induced_network: Vec<usize> =
-            enrich_clustering_with_subnetwork_clusterings_serial(
-                subnetworks,
-                clustering,
-                &nodes_per_cluster,
-                adjusted_resolution,
-                randomness,
-                rng,
-            )?;
+        clustering.reset_next_cluster_id();
 
-        let induced_clustering_network: Network = network.induce_clustering_network(clustering)?;
+        let mut num_nodes_per_cluster_induced_network: Vec<usize> =
+            Vec::with_capacity(num_subnetworks);
+        let max_subnetwork_size: u64 = *num_nodes_per_cluster.iter().max().unwrap();
+        let mut subnetwork_clusterer =
+            SubnetworkClusteringGenerator::with_capacity(max_subnetwork_size as usize);
+
+        for item in subnetworks_iterator {
+            progress_meter!("{}% complete", item.id, num_subnetworks);
+            if num_nodes_per_cluster[item.id] == 1 && item.subnetwork.num_nodes() == 0 {
+                // this is a singleton cluster, and cannot move from what it previously was.
+                // the subnetwork actually has no information about the nodes in it, because we don't
+                // store nodes without neighbors in the network objects, so instead we need to ask the iterator
+                // for some internal state
+                let single_node_vec: &Vec<CompactNodeId> = &nodes_by_cluster[item.id];
+                // manually merge this into the clustering object with the right value
+                let singleton_node: &usize = single_node_vec
+                    .first()
+                    .expect("There should be one node here");
+                clustering.update_cluster_at(*singleton_node, clustering.next_cluster_id())?;
+                num_nodes_per_cluster_induced_network.push(1);
+            } else if item.subnetwork.num_nodes() == 0 {
+                // this is a bug, and we should panic
+                panic!("No node network, which shouldn't have happened");
+            } else {
+                let subnetwork_clustering: Clustering = subnetwork_clusterer
+                    .subnetwork_clustering(
+                        item.subnetwork.compact(),
+                        use_modularity,
+                        adjusted_resolution,
+                        randomness,
+                        rng,
+                    )?;
+                num_nodes_per_cluster_induced_network.push(subnetwork_clustering.next_cluster_id());
+                clustering.merge_subnetwork_clustering(&item.subnetwork, &subnetwork_clustering);
+            }
+        }
+
+        let induced_clustering_network: CompactNetwork =
+            network.induce_clustering_network(clustering)?;
 
         let mut induced_network_clustering = initial_clustering_for_induced(
             num_nodes_per_cluster_induced_network,
@@ -166,6 +200,7 @@ where
         improved |= improve_clustering(
             &induced_clustering_network,
             &mut induced_network_clustering,
+            use_modularity,
             adjusted_resolution,
             randomness,
             rng,
@@ -175,98 +210,6 @@ where
     return Ok(improved);
 }
 
-fn enrich_clustering_with_subnetwork_clusterings_serial<T>(
-    subnetworks: Vec<Network>,
-    clustering: &mut Clustering,
-    nodes_per_cluster: &Vec<Vec<usize>>,
-    adjusted_resolution: f64,
-    randomness: f64,
-    rng: &mut T,
-) -> Result<Vec<usize>, CoreError>
-where
-    T: Rng,
-{
-    let mut num_nodes_per_cluster_induced_network: Vec<usize> =
-        Vec::with_capacity(subnetworks.len());
-    let max_subnetwork_size: usize = subnetworks
-        .iter()
-        .map(|subnetwork| subnetwork.num_nodes())
-        .max()
-        .unwrap();
-
-    let mut subnetwork_clusterer: SubnetworkClusteringGenerator =
-        SubnetworkClusteringGenerator::with_capacity(max_subnetwork_size);
-    log!(
-        "Generating clustering for {} subnetworks",
-        subnetworks.len()
-    );
-    for (i, subnetwork) in subnetworks.iter().enumerate() {
-        progress_meter!("{}% complete", i, subnetworks.len());
-        let subnetwork_clustering: Clustering = subnetwork_clusterer.subnetwork_clustering(
-            subnetwork,
-            adjusted_resolution,
-            randomness,
-            rng,
-        )?;
-        num_nodes_per_cluster_induced_network.push(subnetwork_clustering.next_cluster_id());
-        clustering.merge_subnetwork_clustering(&nodes_per_cluster[i], &subnetwork_clustering);
-    }
-    std::thread::spawn(move || drop(subnetworks));
-    return Ok(num_nodes_per_cluster_induced_network);
-}
-
-// fn update_clustering_by_subnetwork_multi_threaded<T>(
-//     subnetworks: Vec<Network>,
-//     clustering: &mut Clustering,
-//     nodes_per_cluster: &Vec<Vec<usize>>,
-//     adjusted_resolution: f64,
-//     randomness: f64,
-//     rng: &mut T,
-// ) -> Result<Vec<usize>, CoreError>
-// where
-//     T: Rng + Clone + Send,
-// {
-//     let mut subnetworks_and_rng: Vec<(Network, T)> = subnetworks
-//         .into_iter()
-//         .map(|subnetwork| (subnetwork, rng.clone()))
-//         .collect();
-//     let mut num_nodes_per_cluster_induced_network: Vec<usize> = vec![0; subnetworks_and_rng.len()];
-//
-//     let subnetwork_clusterings: Vec<Result<Clustering, CoreError>> = subnetworks_and_rng
-//         .par_iter_mut()
-//         .map(|package| {
-//             let subnetwork: &Network = &package.0;
-//             let rng: &mut T = &mut package.1;
-//             subnetwork_clustering::old_subnetwork_clustering(
-//                 subnetwork,
-//                 adjusted_resolution,
-//                 randomness,
-//                 rng,
-//             )
-//         })
-//         .collect();
-//
-//     let failure: Option<&Result<Clustering, CoreError>> = subnetwork_clusterings
-//         .par_iter()
-//         .find_any(|result: &&Result<Clustering, CoreError>| result.is_err());
-//
-//     if failure.is_some() {
-//         let err: CoreError = failure.unwrap().as_ref().unwrap_err().clone();
-//         return Err(err);
-//     }
-//
-//     subnetwork_clusterings
-//         .iter()
-//         .map(|result| result.as_ref().unwrap())
-//         .enumerate()
-//         .for_each(|(i, clustering_subnetwork)| {
-//             num_nodes_per_cluster_induced_network[i] = clustering_subnetwork.next_cluster_id();
-//             clustering.merge_subnetwork_clustering(&nodes_per_cluster[i], clustering_subnetwork);
-//         });
-//
-//     return Ok(num_nodes_per_cluster_induced_network);
-// }
-
 fn initial_clustering_for_induced(
     num_nodes_per_cluster_induced_network: Vec<usize>,
     num_nodes: usize,
diff --git a/packages/network_partitions/src/leiden/mod.rs b/packages/network_partitions/src/leiden/mod.rs
index 6d1b9f9..6ecba9b 100644
--- a/packages/network_partitions/src/leiden/mod.rs
+++ b/packages/network_partitions/src/leiden/mod.rs
@@ -1,10 +1,12 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT license.
 
+pub use self::hierarchical::{hierarchical_leiden, HierarchicalCluster};
 pub use self::leiden::leiden;
 
 mod full_network_clustering;
 mod full_network_work_queue;
+mod hierarchical;
 mod leiden;
 mod neighboring_clusters;
 mod quality_value_increment;
diff --git a/packages/network_partitions/src/leiden/subnetwork.rs b/packages/network_partitions/src/leiden/subnetwork.rs
index b7f3417..9f2482d 100644
--- a/packages/network_partitions/src/leiden/subnetwork.rs
+++ b/packages/network_partitions/src/leiden/subnetwork.rs
@@ -6,7 +6,7 @@ use rand::Rng;
 use super::quality_value_increment::calculate;
 use crate::clustering::Clustering;
 use crate::errors::CoreError;
-use crate::network::Network;
+use crate::network::prelude::*;
 
 pub const DEFAULT_RANDOMNESS: f64 = 1e-2;
 
@@ -42,7 +42,8 @@ impl SubnetworkClusteringGenerator {
 
     pub fn subnetwork_clustering<T>(
         &mut self,
-        subnetwork: &Network,
+        subnetwork: &CompactNetwork,
+        use_modularity: bool,
         adjusted_resolution: f64,
         randomness: f64,
         rng: &mut T,
@@ -59,8 +60,11 @@ impl SubnetworkClusteringGenerator {
         let mut improved: bool = false;
 
         let mut cluster_weights: Vec<f64> = subnetwork.node_weights();
-        let mut external_edge_weight_per_cluster: Vec<f64> =
-            subnetwork.total_edge_weight_per_node();
+        let mut external_edge_weight_per_cluster: Vec<f64> = if use_modularity {
+            subnetwork.node_weights()
+        } else {
+            subnetwork.total_edge_weight_per_node()
+        };
         let total_node_weight: f64 = subnetwork.total_node_weight();
 
         let neighboring_clusters: &mut Vec<usize> = self.neighboring_clusters.as_mut();
@@ -89,19 +93,17 @@ impl SubnetworkClusteringGenerator {
                 cluster_weights[node] = 0_f64;
                 external_edge_weight_per_cluster[node] = 0_f64;
 
-                let (start_neighbor_index, end_neighbor_index) = subnetwork.neighbor_range(node)?;
-                for neighbor_index in start_neighbor_index..end_neighbor_index {
-                    let (neighbor, weight) = subnetwork.edge_at(neighbor_index)?;
-                    let neighbor_cluster: usize = clustering.cluster_at(neighbor)?;
+                for neighbor in subnetwork.neighbors_for(node) {
+                    let neighbor_cluster: usize = clustering.cluster_at(neighbor.id)?;
                     if neighboring_cluster_edge_weights[neighbor_cluster] == 0_f64 {
                         neighboring_clusters.push(neighbor_cluster);
                     }
-                    neighboring_cluster_edge_weights[neighbor_cluster] += weight;
+                    neighboring_cluster_edge_weights[neighbor_cluster] += neighbor.edge_weight;
                 }
 
                 let chosen_cluster: usize = best_cluster_for_node(
                     node,
-                    subnetwork.node_weight_at(node)?,
+                    subnetwork.node(node).weight,
                     &neighboring_clusters,
                     neighboring_cluster_edge_weights,
                     &cluster_weights,
@@ -112,17 +114,16 @@ impl SubnetworkClusteringGenerator {
                     randomness,
                     rng,
                 );
-                cluster_weights[chosen_cluster] += subnetwork.node_weight_at(node)?;
+                cluster_weights[chosen_cluster] += subnetwork.node_weight(node);
 
                 // TODO: Literally none of this makes sense.  Why would we decrement the edge
                 // weight of all nodes in a cluster IF they match, otherwise we increment them?
                 // this just doesn't make sense
-                for neighbor_index in start_neighbor_index..end_neighbor_index {
-                    let (neighbor, edge_weight) = subnetwork.edge_at(neighbor_index)?;
-                    if clustering.cluster_at(neighbor)? == chosen_cluster {
-                        external_edge_weight_per_cluster[chosen_cluster] -= edge_weight;
+                for neighbor in subnetwork.neighbors_for(node) {
+                    if clustering.cluster_at(neighbor.id)? == chosen_cluster {
+                        external_edge_weight_per_cluster[chosen_cluster] -= neighbor.edge_weight;
                     } else {
-                        external_edge_weight_per_cluster[chosen_cluster] += edge_weight;
+                        external_edge_weight_per_cluster[chosen_cluster] += neighbor.edge_weight;
                     }
                 }
 
diff --git a/packages/network_partitions/src/network/compact_network.rs b/packages/network_partitions/src/network/compact_network.rs
new file mode 100644
index 0000000..d02dc51
--- /dev/null
+++ b/packages/network_partitions/src/network/compact_network.rs
@@ -0,0 +1,668 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+/// A CompactNetwork is a tightly packed neighbor-optimized datastructure that defines a
+/// *undirected* Network's edges.
+///
+/// The structure is optimized for:
+///  - Fast lookups of all the neighbors (and the edges' weights) for a given Node
+///  - Iteration through all the Nodes in the Network
+///  - Iteration through all the neighbors (and edge weights) for each Node
+///
+/// To that end, the structure is primarily defined as two related collections:
+///  - A vector whose indices are the NodeIds (usize) and whose values are the first EdgeId in the
+///    second collection
+///  - A second vector whose indices are the EdgeIds (usize) and whose values are a tuple of
+///    (NodeId, weight (f64)).
+///  The second vector's entries make two guarantees: that all of the neighbors for a given node
+///  will be continuous, and that the neighbors will be sorted in ascending order as per NodeId.
+///
+/// This layout will hopefully minimize memory paging with cache misses by locating all of the
+/// edges for a given node within the fewest pages possible vs. a conceptually simpler pointer based
+/// approach.
+///
+/// This also means that the neighbors for a given NodeId `x` can be described roughly as:
+///  - `second_vec[first_vec[x]..first_vec[x+1]]` (except when x = first_vec.len() - 1)
+///
+/// These Vectors also allow us to determine the number of nodes (first_vec.len()) and the
+/// number of edges (second_vec.len() / 2) very quickly.
+///
+/// Desired behaviors:
+///  - Iterate through CompactNetwork by a tuple of (NodeId, NeighborIterator)
+///  - NeighborIterator must yield a tuple of:
+///    - EdgeId (index of the edge in the array)
+///    - Neighbor's NodeId
+///    - Edge weight (f64)
+///
+/// e.g. a Network defined by the edges:
+///   0,1,3.0
+///   0,2,2.1
+///   1,0,3.0
+///   1,2,5.0
+///   2,0,2.1
+///   2,1,5.0
+///
+/// And the following code:
+/// for (node_id, neighbor_iter) in &compact_network {
+///   println!("Node ID: {}", node_id);
+///   println!("Edges: ");
+///   for (edge_id, (neighbor_node_id, edge_weight)) in neighbor_iter {
+///     println!("\t{}: {} @ index {}", neighbor_node_id, edge_weight, edge_id);
+///   }
+/// }
+///
+/// Would print:
+///   Node ID: 0
+///   Edges:
+///       1: 3.0 @ index 0
+///       2: 2.1 @ index 1
+///   Node ID: 1
+///   Edges:
+///       0: 3.0 @ index 2
+///       2: 5.0 @ index 3
+///   Node ID: 2
+///   Edges:
+///       0: 2.1 @ index 4
+///       1: 5.0 @ index 5
+///
+/// The EdgeId is the direct index into the edges array for fast lookups of a specific edge,
+/// which is primarily useful to the subnetwork generation functions
+use super::networks::NetworkDetails;
+use crate::clustering::Clustering;
+use crate::errors::CoreError;
+use crate::network::{LabeledNetwork, LabeledNetworkBuilder};
+use std::collections::HashMap;
+use std::ops::Range;
+
+// Simple types that can be exposed
+pub type CompactNodeId = usize;
+pub type ClusterId = usize;
+pub type ConnectionId = usize; // note: Maybe we shouldn't expose this?  It's not super pertinent outside of this module
+
+// the combination of these tuples plus the vector index associated with them makes the public struct
+pub type CompactNode = (f64, ConnectionId);
+pub type CompactNeighbor = (CompactNodeId, f64);
+
+#[derive(Debug)]
+pub struct CompactNodeItem<'a> {
+    pub id: CompactNodeId,
+    pub weight: f64,
+    compact_network: &'a CompactNetwork,
+}
+
+impl<'a> CompactNodeItem<'a> {
+    pub fn neighbors(&self) -> NeighborIterator<'a> {
+        // make the neighbor iterator here, not on compactnodeitem creation
+        let neighbor_range: Range<ConnectionId> = self.compact_network.neighbor_range(self.id);
+        let neighbor_start: ConnectionId = neighbor_range.start;
+        return NeighborIterator {
+            compact_network: self.compact_network,
+            neighbor_range,
+            current_neighbor: neighbor_start,
+        };
+    }
+}
+
+#[derive(Debug)]
+pub struct CompactNeighborItem {
+    pub connection_id: ConnectionId, // may be unnecessary
+    pub id: CompactNodeId,
+    pub edge_weight: f64,
+    pub node_weight: f64,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct CompactNetwork {
+    nodes: Vec<CompactNode>,
+    neighbors: Vec<CompactNeighbor>,
+    total_self_links_edge_weight: f64,
+}
+
+#[derive(Debug)]
+pub struct CompactSubnetworkItem<T> {
+    pub id: ClusterId,
+    pub subnetwork: LabeledNetwork<T>,
+}
+
+impl CompactNetwork {
+    pub fn from(
+        nodes: Vec<CompactNode>,
+        neighbors: Vec<CompactNeighbor>,
+        total_self_links_edge_weight: f64,
+    ) -> CompactNetwork {
+        return CompactNetwork {
+            nodes,
+            neighbors,
+            total_self_links_edge_weight,
+        };
+    }
+
+    fn neighbor_range(
+        &self,
+        node_id: CompactNodeId,
+    ) -> Range<CompactNodeId> {
+        let (_, neighbor_start) = self.nodes[node_id];
+        let end_range: ConnectionId = if node_id < self.nodes.len() - 1 {
+            self.nodes[node_id + 1].1
+        } else {
+            self.neighbors.len()
+        };
+        return neighbor_start..end_range;
+    }
+
+    pub fn node(
+        &self,
+        id: CompactNodeId,
+    ) -> CompactNodeItem {
+        let weight: &f64 = &self.nodes[id].0;
+        return CompactNodeItem {
+            id,
+            weight: *weight,
+            compact_network: self,
+        };
+    }
+
+    pub fn neighbors_for(
+        &self,
+        id: CompactNodeId,
+    ) -> NeighborIterator {
+        let neighbor_range: Range<ConnectionId> = self.neighbor_range(id);
+        let neighbor_start: ConnectionId = neighbor_range.start;
+        return NeighborIterator {
+            compact_network: self,
+            neighbor_range,
+            current_neighbor: neighbor_start,
+        };
+    }
+
+    pub fn node_weight(
+        &self,
+        id: CompactNodeId,
+    ) -> f64 {
+        return self.nodes[id].0;
+    }
+
+    pub fn node_weights(&self) -> Vec<f64> {
+        return self.nodes.iter().map(|(weight, _)| *weight).collect();
+    }
+
+    pub fn total_edge_weight_per_node(&self) -> Vec<f64> {
+        // when using modularity, this should return the exact same as node_weights.
+        return self
+            .nodes
+            .iter()
+            .map(|(_, node_id)| {
+                self.neighbors_for(*node_id)
+                    .map(|neighbor| neighbor.edge_weight)
+                    .sum::<f64>()
+            })
+            .collect();
+    }
+
+    pub fn subnetworks_iter<'a, 'b>(
+        &'a self,
+        clustering: &Clustering,
+        nodes_by_cluster: &'b Vec<Vec<CompactNodeId>>,
+        subnetwork_minimum_size: Option<u32>,
+    ) -> SubnetworkIterator<'a, 'b> {
+        let clustering: Clustering = clustering.clone();
+
+        return SubnetworkIterator {
+            compact_supernetwork: self,
+            clustering,
+            clustered_nodes: nodes_by_cluster,
+            current_clustered_nodes_index: 0,
+            builder: LabeledNetworkBuilder::new(),
+            subnetwork_minimum_size,
+        };
+    }
+
+    pub fn filtered_subnetworks<'a>(
+        &'a self,
+        clustering: &'a Clustering,
+        nodes_by_cluster: &'a Vec<Vec<CompactNodeId>>,
+        subnetwork_minimum_size: u32,
+        use_modularity: bool,
+    ) -> impl Iterator<Item = CompactSubnetworkItem<CompactNodeId>> + 'a {
+        let mut labeled_network_builder: LabeledNetworkBuilder<CompactNodeId> =
+            LabeledNetworkBuilder::new();
+        let subnetwork_iterator = nodes_by_cluster
+            .iter()
+            .enumerate()
+            .filter(move |(_cluster_id, nodes_in_cluster)| {
+                nodes_in_cluster.len() >= subnetwork_minimum_size as usize
+            })
+            .map(move |(cluster_id, nodes_in_cluster)| {
+                let subnetwork_edges = nodes_in_cluster.into_iter().flat_map(|node| {
+                    self.neighbors_for(*node)
+                        .filter(|neighbor| clustering[neighbor.id] == cluster_id)
+                        .map(move |neighbor| (*node, neighbor.id, neighbor.edge_weight))
+                });
+                let subnetwork: LabeledNetwork<CompactNodeId> =
+                    labeled_network_builder.build(subnetwork_edges, use_modularity);
+                CompactSubnetworkItem {
+                    subnetwork,
+                    id: cluster_id,
+                }
+            });
+        return subnetwork_iterator;
+    }
+
+    pub fn induce_clustering_network(
+        &self,
+        clustering: &Clustering,
+    ) -> Result<CompactNetwork, CoreError> {
+        let mut cluster_weights: Vec<f64> = vec![0_f64; clustering.next_cluster_id()];
+        let mut cluster_total_self_links_edge_weight = self.total_self_links_edge_weight();
+
+        let mut cluster_to_cluster_edges: HashMap<CompactNodeId, HashMap<CompactNodeId, f64>> =
+            HashMap::new();
+
+        for (node_id, (node_weight, _)) in self.nodes.iter().enumerate() {
+            let node_cluster: CompactNodeId = clustering.cluster_at(node_id)?;
+            cluster_weights[node_cluster] += node_weight;
+            for neighbor in self.neighbors_for(node_id) {
+                let neighbor_cluster: CompactNodeId = clustering.cluster_at(neighbor.id)?;
+                if node_cluster == neighbor_cluster {
+                    cluster_total_self_links_edge_weight += neighbor.edge_weight;
+                } else {
+                    *cluster_to_cluster_edges
+                        .entry(node_cluster)
+                        .or_insert(HashMap::new())
+                        .entry(neighbor_cluster)
+                        .or_insert(0_f64) += neighbor.edge_weight;
+                }
+            }
+        }
+
+        let mut cluster_nodes: Vec<CompactNode> = Vec::with_capacity(clustering.next_cluster_id());
+        let mut cluster_neighbors: Vec<CompactNeighbor> = Vec::new();
+
+        for cluster in 0..clustering.next_cluster_id() {
+            cluster_nodes.push((cluster_weights[cluster], cluster_neighbors.len()));
+            let mut neighbors: Vec<(&usize, &f64)> = cluster_to_cluster_edges
+                .entry(cluster)
+                .or_insert(HashMap::new())
+                .iter()
+                .collect();
+            neighbors.sort_unstable_by(|a, b| a.0.cmp(b.0));
+            cluster_neighbors.reserve(neighbors.len());
+            for (neighbor_cluster, edge_weight) in neighbors {
+                cluster_neighbors.push((*neighbor_cluster, *edge_weight));
+            }
+        }
+
+        let induced: CompactNetwork = CompactNetwork::from(
+            cluster_nodes,
+            cluster_neighbors,
+            cluster_total_self_links_edge_weight,
+        );
+
+        return Ok(induced);
+    }
+}
+
+impl NetworkDetails for CompactNetwork {
+    fn num_nodes(&self) -> usize {
+        return self.nodes.len();
+    }
+
+    fn num_edges(&self) -> usize {
+        return (self.neighbors.len() as f64 / 2_f64) as usize;
+    }
+
+    fn total_node_weight(&self) -> f64 {
+        return self.nodes.iter().map(|node| node.0).sum::<f64>();
+    }
+
+    fn total_edge_weight(&self) -> f64 {
+        return self
+            .neighbors
+            .iter()
+            .map(|neighbor| neighbor.1)
+            .sum::<f64>()
+            / 2_f64;
+    }
+
+    fn total_self_links_edge_weight(&self) -> f64 {
+        return self.total_self_links_edge_weight;
+    }
+}
+
+impl<'a> IntoIterator for &'a CompactNetwork {
+    type Item = CompactNodeItem<'a>;
+    type IntoIter = NodeIterator<'a>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        return NodeIterator {
+            compact_network: &self,
+            current_node: 0,
+        };
+    }
+}
+
+pub struct NodeIterator<'a> {
+    compact_network: &'a CompactNetwork,
+    current_node: CompactNodeId,
+}
+
+impl<'a> Iterator for NodeIterator<'a> {
+    type Item = CompactNodeItem<'a>;
+    fn next(&mut self) -> Option<Self::Item> {
+        return if self.current_node == self.compact_network.nodes.len() {
+            None
+        } else {
+            let item = self.compact_network.node(self.current_node);
+            self.current_node += 1;
+            Some(item)
+        };
+    }
+}
+
+#[derive(Debug)]
+pub struct NeighborIterator<'a> {
+    compact_network: &'a CompactNetwork,
+    neighbor_range: Range<ConnectionId>,
+    current_neighbor: ConnectionId,
+}
+
+impl<'a> Iterator for NeighborIterator<'a> {
+    type Item = CompactNeighborItem;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        return if self.neighbor_range.contains(&self.current_neighbor) {
+            let (neighbor_id, edge_weight) = self.compact_network.neighbors[self.current_neighbor];
+            let item = CompactNeighborItem {
+                connection_id: self.current_neighbor,
+                id: neighbor_id,
+                edge_weight,
+                node_weight: self.compact_network.nodes[neighbor_id].0,
+            };
+            self.current_neighbor += 1;
+            Some(item)
+        } else {
+            None
+        };
+    }
+}
+
+pub struct SubnetworkIterator<'a, 'b> {
+    compact_supernetwork: &'a CompactNetwork, // original data to project a new subnetwork from
+    clustering: Clustering,
+    clustered_nodes: &'b Vec<Vec<CompactNodeId>>,
+    current_clustered_nodes_index: usize, // current clustered nodes to process.  Note that this index does not necessarily imply it is the ClusterId.
+    builder: LabeledNetworkBuilder<CompactNodeId>,
+    subnetwork_minimum_size: Option<u32>,
+}
+
+impl<'a, 'b> Iterator for SubnetworkIterator<'a, 'b> {
+    type Item = CompactSubnetworkItem<CompactNodeId>;
+    fn next(&mut self) -> Option<Self::Item> {
+        let next_valid_position: Option<usize> = match self.subnetwork_minimum_size {
+            None => {
+                if self.current_clustered_nodes_index == self.clustered_nodes.len() {
+                    None
+                } else {
+                    Some(self.current_clustered_nodes_index)
+                }
+            }
+            Some(subnetwork_minimum_size) => {
+                let mut found: bool = false;
+                let mut possibly_valid: usize = self.current_clustered_nodes_index;
+                while possibly_valid != self.clustered_nodes.len() && !found {
+                    if self.clustered_nodes[possibly_valid].len()
+                        >= subnetwork_minimum_size as usize
+                    {
+                        found = true;
+                    } else {
+                        possibly_valid += 1;
+                    }
+                }
+                if found {
+                    Some(possibly_valid)
+                } else {
+                    None
+                }
+            }
+        };
+        return match next_valid_position {
+            Some(current) => {
+                self.current_clustered_nodes_index = current;
+
+                let nodes = &self.clustered_nodes[current];
+                let cluster_id: usize = current;
+
+                let edges: Vec<(CompactNodeId, CompactNodeId, f64)> = nodes
+                    .iter()
+                    .flat_map(|node_in_cluster| {
+                        // get all neighbors that belong to it and belong in the same cluster
+                        self.compact_supernetwork
+                            .neighbors_for(*node_in_cluster)
+                            .map(move |neighbor| {
+                                (*node_in_cluster, neighbor.id, neighbor.edge_weight)
+                            })
+                            .filter(|edge| {
+                                let edge: (CompactNodeId, CompactNodeId, f64) = *edge;
+                                self.clustering[edge.1] == cluster_id
+                            })
+                    })
+                    .collect();
+
+                let labeled_network: LabeledNetwork<CompactNodeId> =
+                    self.builder.build(edges.into_iter(), true);
+
+                self.current_clustered_nodes_index += 1;
+                Some(CompactSubnetworkItem {
+                    id: cluster_id,
+                    subnetwork: labeled_network,
+                })
+            }
+            None => {
+                self.current_clustered_nodes_index = self.clustered_nodes.len();
+                None
+            }
+        };
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+
+    /*
+       edges
+       0, 1, 2.0
+       0, 3, 1.0
+       0, 4, 1.0
+       1, 0, 2.0
+       1, 2, 6.0
+       1, 4, 1.0
+       1, 5, 4.0
+       1, 6, 3.0
+       2, 1, 6.0
+       2, 6, 3.0
+       3, 0, 1.0
+       3, 7, 11.0
+       4, 0, 1.0
+       4, 1, 1.0
+       5, 1, 4.0
+       6, 1, 3.0
+       6, 2, 3.0
+       7, 3, 11.0
+    */
+
+    fn fresh_network(self_links: f64) -> CompactNetwork {
+        let nodes: Vec<CompactNode> = vec![
+            (4_f64, 0),
+            (16_f64, 3),
+            (9_f64, 8),
+            (12_f64, 10),
+            (2_f64, 12),
+            (4_f64, 14),
+            (6_f64, 15),
+            (11_f64, 17),
+        ];
+        let neighbors: Vec<CompactNeighbor> = vec![
+            (1, 2_f64), // 0
+            (3, 1_f64),
+            (4, 1_f64),
+            (0, 2_f64), // 1
+            (2, 6_f64),
+            (4, 1_f64),
+            (5, 4_f64),
+            (6, 3_f64),
+            (1, 6_f64), // 2
+            (6, 3_f64),
+            (0, 1_f64), // 3
+            (7, 11_f64),
+            (0, 1_f64), // 4
+            (1, 1_f64),
+            (1, 4_f64), // 5
+            (1, 3_f64), // 6
+            (2, 3_f64),
+            (3, 11_f64), // 7
+        ];
+        return CompactNetwork::from(nodes, neighbors, self_links);
+    }
+
+    #[test]
+    fn test_node_iterator() {
+        let network: CompactNetwork = fresh_network(0_f64);
+        // testing the neighbor iterator is in another test, so we're only validating the IDs and the node weights in this case.
+        let expected_like: Vec<(CompactNodeId, f64)> = vec![
+            (0, 4_f64),
+            (1, 16_f64),
+            (2, 9_f64),
+            (3, 12_f64),
+            (4, 2_f64),
+            (5, 4_f64),
+            (6, 6_f64),
+            (7, 11_f64),
+        ];
+        let mut index: usize = 0;
+        for CompactNodeItem { id, weight, .. } in &network {
+            let (expected_id, expected_weight) = expected_like[index];
+            assert_eq!(expected_id, id);
+            assert_eq!(expected_weight, weight);
+            index += 1;
+        }
+        assert_eq!(8, index);
+    }
+
+    #[test]
+    fn test_neighbor_iterator() {
+        let network: CompactNetwork = fresh_network(0_f64);
+        let expected_neighbors: Vec<(CompactNodeId, f64)> =
+            vec![(0, 2_f64), (2, 6_f64), (4, 1_f64), (5, 4_f64), (6, 3_f64)];
+        let node = network.node(1);
+        let actual: Vec<CompactNeighborItem> = node.neighbors().collect();
+        assert_eq!(5, actual.len());
+
+        for (index, neighbor) in actual.into_iter().enumerate() {
+            let (expected_neighbor_id, expected_edge_weight) = expected_neighbors[index];
+            assert_eq!(expected_neighbor_id, neighbor.id);
+            assert_eq!(expected_edge_weight, neighbor.edge_weight);
+        }
+    }
+
+    // #[test]
+    // fn test_subnetwork_iterator() {
+    //     let network: CompactNetwork = fresh_network(1_f64);
+    //     let node_to_cluster: Vec<usize> = vec![0, 1, 1, 0, 0, 1, 1, 0];
+    //
+    //     let clustering: Clustering = Clustering::as_defined(node_to_cluster, 2);
+    //
+    //     let subnetwork1: CompactNetwork = CompactNetwork {
+    //         nodes: vec![(2_f64, 0), (12_f64, 2), (1_f64, 4), (11_f64, 5)],
+    //         neighbors: vec![
+    //             (1, 1_f64),
+    //             (2, 1_f64),
+    //             (0, 1_f64),
+    //             (3, 11_f64),
+    //             (0, 1_f64),
+    //             (1, 11_f64),
+    //         ],
+    //         total_self_links_edge_weight: 0_f64,
+    //     };
+    //     let subnetwork2: CompactNetwork = CompactNetwork {
+    //         nodes: vec![(13_f64, 0), (9_f64, 3), (4_f64, 5), (6_f64, 6)],
+    //         neighbors: vec![
+    //             (1, 6_f64),
+    //             (2, 4_f64),
+    //             (3, 3_f64),
+    //             (0, 6_f64),
+    //             (3, 3_f64),
+    //             (0, 4_f64),
+    //             (0, 3_f64),
+    //             (1, 3_f64),
+    //         ],
+    //         total_self_links_edge_weight: 0_f64,
+    //     };
+    //     let expected: Vec<CompactSubnetworkItem<CompactNodeId>> = vec![
+    //         CompactSubnetworkItem {
+    //             id: 0,
+    //             subnetwork: CompactSubnetwork {
+    //                 compact_network: subnetwork1,
+    //                 node_id_map: vec![0, 3, 4, 7],
+    //             },
+    //         },
+    //         CompactSubnetworkItem {
+    //             id: 1,
+    //             subnetwork: CompactSubnetwork {
+    //                 compact_network: subnetwork2,
+    //                 node_id_map: vec![1, 2, 5, 6],
+    //             },
+    //         },
+    //     ];
+    //     let actual: Vec<CompactSubnetworkItem<CompactNodeId>> =
+    //         network.subnetworks_iter(&clustering, None).collect();
+    //     assert_eq!(expected, actual);
+    // }
+    //
+    // #[test]
+    // fn test_subnetwork_filters() {
+    //     let network: CompactNetwork = fresh_network(1_f64);
+    //     let node_to_cluster: Vec<usize> = vec![0, 1, 1, 0, 0, 1, 1, 2];
+    //
+    //     let clustering: Clustering = Clustering::as_defined(node_to_cluster, 3);
+    //
+    //     let subnetwork1: CompactNetwork = CompactNetwork {
+    //         nodes: vec![(2_f64, 0), (1_f64, 2), (1_f64, 3)],
+    //         neighbors: vec![(1, 1_f64), (2, 1_f64), (0, 1_f64), (0, 1_f64)],
+    //         total_self_links_edge_weight: 0_f64,
+    //     };
+    //     let subnetwork2: CompactNetwork = CompactNetwork {
+    //         nodes: vec![(13_f64, 0), (9_f64, 3), (4_f64, 5), (6_f64, 6)],
+    //         neighbors: vec![
+    //             (1, 6_f64),
+    //             (2, 4_f64),
+    //             (3, 3_f64),
+    //             (0, 6_f64),
+    //             (3, 3_f64),
+    //             (0, 4_f64),
+    //             (0, 3_f64),
+    //             (1, 3_f64),
+    //         ],
+    //         total_self_links_edge_weight: 0_f64,
+    //     };
+    //     let expected: Vec<CompactSubnetworkItem<CompactNodeId>> = vec![
+    //         CompactSubnetworkItem {
+    //             id: 0,
+    //             subnetwork: CompactSubnetwork {
+    //                 compact_network: subnetwork1,
+    //                 node_id_map: vec![0, 3, 4],
+    //             },
+    //         },
+    //         CompactSubnetworkItem {
+    //             id: 1,
+    //             subnetwork: CompactSubnetwork {
+    //                 compact_network: subnetwork2,
+    //                 node_id_map: vec![1, 2, 5, 6],
+    //             },
+    //         },
+    //     ];
+    //     let actual: Vec<CompactSubnetworkItem<CompactNodeId>> =
+    //         network.subnetworks_iter(&clustering, Some(3)).collect();
+    //     assert_eq!(expected, actual);
+    // }
+}
diff --git a/packages/network_partitions/src/network/identifier.rs b/packages/network_partitions/src/network/identifier.rs
new file mode 100644
index 0000000..b8125be
--- /dev/null
+++ b/packages/network_partitions/src/network/identifier.rs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+use std::collections::HashMap;
+use std::hash::Hash;
+
+pub struct Identifier<T> {
+    original_to_new: HashMap<T, usize>,
+    new_to_original: Vec<T>,
+}
+
+impl<T> Identifier<T>
+where
+    T: Clone + Hash + Eq,
+{
+    pub fn new() -> Self {
+        let map: HashMap<T, usize> = HashMap::new();
+        let id: Identifier<T> = Identifier {
+            original_to_new: map,
+            new_to_original: Vec::new(),
+        };
+        return id;
+    }
+
+    pub fn identify(
+        &mut self,
+        original: T,
+    ) -> usize {
+        return match self.original_to_new.get(&original) {
+            Some(id) => *id,
+            None => {
+                let new_id: usize = self.new_to_original.len();
+                self.original_to_new.insert(original.clone(), new_id);
+                self.new_to_original.push(original);
+                new_id
+            }
+        };
+    }
+
+    pub fn identity_map(&self) -> Vec<T> {
+        return self.new_to_original.clone();
+    }
+
+    pub fn clear(&mut self) {
+        self.new_to_original.clear();
+        self.original_to_new.clear();
+    }
+
+    pub fn finish(&mut self) -> (HashMap<T, usize>, Vec<T>) {
+        let id_to_label: HashMap<T, usize> = self.original_to_new.clone();
+        let label_to_id: Vec<T> = self.new_to_original.clone();
+
+        self.original_to_new.clear();
+        self.new_to_original.clear();
+
+        return (id_to_label, label_to_id);
+    }
+}
diff --git a/packages/network_partitions/src/network/labeled_network.rs b/packages/network_partitions/src/network/labeled_network.rs
new file mode 100644
index 0000000..f538d50
--- /dev/null
+++ b/packages/network_partitions/src/network/labeled_network.rs
@@ -0,0 +1,338 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+use std::collections::HashMap;
+use std::fs::File;
+use std::io::{BufReader, Read};
+
+use crate::errors::NetworkError;
+
+use super::compact_network::{CompactNeighbor, CompactNetwork, CompactNode, CompactNodeId};
+use super::networks::NetworkDetails;
+use super::{Edge, Identifier};
+
+use std::hash::Hash;
+
+#[derive(Debug)]
+pub struct LabeledNetwork<T> {
+    network_structure: CompactNetwork,
+    labels_to_id: HashMap<T, CompactNodeId>,
+    id_to_labels: Vec<T>,
+}
+
+impl<T> NetworkDetails for LabeledNetwork<T> {
+    fn num_nodes(&self) -> usize {
+        return self.network_structure.num_nodes();
+    }
+
+    fn num_edges(&self) -> usize {
+        return self.network_structure.num_edges();
+    }
+
+    fn total_node_weight(&self) -> f64 {
+        return self.network_structure.total_node_weight();
+    }
+
+    fn total_edge_weight(&self) -> f64 {
+        return self.network_structure.total_edge_weight();
+    }
+
+    fn total_self_links_edge_weight(&self) -> f64 {
+        return self.network_structure.total_self_links_edge_weight();
+    }
+}
+
+pub struct LabeledNetworkBuilder<T> {
+    node_to_neighbors: HashMap<CompactNodeId, HashMap<CompactNodeId, f64>>,
+    identifier: Identifier<T>,
+}
+
+impl<T> LabeledNetworkBuilder<T>
+where
+    T: Clone + Eq + Hash + PartialEq + std::cmp::PartialEq,
+{
+    pub fn new() -> Self {
+        let builder: LabeledNetworkBuilder<T> = LabeledNetworkBuilder {
+            node_to_neighbors: HashMap::new(),
+            identifier: Identifier::new(),
+        };
+        return builder;
+    }
+
+    pub fn with_capacity(size: usize) -> Self {
+        let builder: LabeledNetworkBuilder<T> = LabeledNetworkBuilder {
+            node_to_neighbors: HashMap::with_capacity(size),
+            identifier: Identifier::new(),
+        };
+        return builder;
+    }
+
+    pub fn build<I>(
+        &mut self,
+        edges_iter: I,
+        use_modularity: bool,
+    ) -> LabeledNetwork<T>
+    where
+        I: Iterator<Item = (T, T, f64)>,
+    {
+        // set up our working area first
+        self.node_to_neighbors.clear();
+        self.identifier.clear();
+
+        let node_to_neighbors: &mut HashMap<CompactNodeId, HashMap<CompactNodeId, f64>> =
+            &mut self.node_to_neighbors;
+
+        // set up our target vectors for the CompactNetwork
+        let mut nodes: Vec<CompactNode> = Vec::new();
+        let mut neighbors: Vec<CompactNeighbor> = Vec::new();
+
+        for (source, target, weight) in edges_iter {
+            // as we see nodes, we're going to give them an identity, either by reusing one already
+            // given or by creating a new one in the order first seen in the edge list.
+            let source_id: CompactNodeId = self.identifier.identify(source);
+            let target_id: CompactNodeId = self.identifier.identify(target);
+            node_to_neighbors
+                .entry(source_id)
+                .or_insert(HashMap::new())
+                .entry(target_id)
+                .or_insert(weight);
+            node_to_neighbors
+                .entry(target_id)
+                .or_insert(HashMap::new())
+                .entry(source_id)
+                .or_insert(weight);
+        }
+
+        let (labels_to_id, id_to_labels) = self.identifier.finish();
+
+        let mut total_self_links_edge_weight: f64 = 0_f64;
+        for node_id in 0..id_to_labels.len() {
+            let mut node_weight: f64 = 0_f64; // we are going to set the node_weight as the summation of edge weights regardless of whether we're using modularity or CPM, but if we are using CPM we won't bother to use it.
+            let mut node_neighbors: Vec<(&CompactNodeId, &f64)> = node_to_neighbors
+                .get(&node_id)
+                .unwrap()
+                .into_iter()
+                .collect();
+            let neighbor_start: usize = neighbors.len();
+            node_neighbors.sort_by(|a, b| a.0.cmp(b.0));
+            for (neighbor_id, edge_weight) in node_neighbors {
+                if *neighbor_id == node_id {
+                    total_self_links_edge_weight += *edge_weight;
+                } else {
+                    node_weight += *edge_weight; // TODO: do we do this as well even if it's a self link?
+                    neighbors.push((*neighbor_id, *edge_weight));
+                }
+            }
+            let node_weight: f64 = if use_modularity { node_weight } else { 1_f64 };
+            nodes.push((node_weight, neighbor_start));
+        }
+
+        let compact_network: CompactNetwork =
+            CompactNetwork::from(nodes, neighbors, total_self_links_edge_weight);
+
+        let labeled_network: LabeledNetwork<T> = LabeledNetwork {
+            labels_to_id,
+            id_to_labels,
+            network_structure: compact_network,
+        };
+
+        return labeled_network;
+    }
+}
+
+impl<T> LabeledNetwork<T>
+where
+    T: Clone + Eq + Hash + PartialEq + std::cmp::PartialEq,
+{
+    /// Superficially this seems like an easy task. Get the edges, add them. But we don't *know*
+    /// that the edges provided are already in sorted source order (e.g. all edges from A to <N>
+    /// all appear sequentially in the list.
+    /// So we must collect and guarantee that behavior with this function.
+
+    pub fn compact(&self) -> &CompactNetwork {
+        return &self.network_structure;
+    }
+
+    pub fn compact_id_for(
+        &self,
+        id: T,
+    ) -> Option<CompactNodeId> {
+        return self.labels_to_id.get(&id).cloned();
+    }
+
+    pub fn label_for(
+        &self,
+        compact_id: CompactNodeId,
+    ) -> &T {
+        return &self.id_to_labels[compact_id];
+    }
+
+    pub fn labeled_ids(&self) -> impl Iterator<Item = (CompactNodeId, &T)> + '_ {
+        return self.id_to_labels.iter().enumerate();
+    }
+
+    pub fn load_from(
+        path: &str,
+        separator: &str,
+        source_index: usize,
+        target_index: usize,
+        weight_index: Option<usize>,
+        skip_first_line: bool,
+        use_modularity: bool,
+    ) -> Result<LabeledNetwork<String>, NetworkError> {
+        let minimum_required_length: usize = source_index
+            .max(target_index)
+            .max(weight_index.unwrap_or(target_index))
+            + 1;
+        let mut reader: BufReader<File> = BufReader::new(File::open(path)?);
+        let mut contents = String::new();
+        reader.read_to_string(&mut contents)?;
+        let skip_lines: usize = if skip_first_line { 1 } else { 0 };
+        let mut edges: Vec<Edge> = Vec::new();
+        for line in contents.lines().skip(skip_lines) {
+            if !line.is_empty() {
+                let splits: Vec<&str> = line.split(separator).collect();
+                if splits.len() < minimum_required_length {
+                    return Err(NetworkError::EdgeFileFormatError);
+                }
+                let source: &str = splits[source_index];
+                let target: &str = splits[target_index];
+                let weight: f64 = match weight_index {
+                    Some(weight_index) => splits[weight_index]
+                        .parse::<f64>()
+                        .map_err(|_err| NetworkError::EdgeFileFormatError)?,
+                    None => 1_f64,
+                };
+                edges.push((source.into(), target.into(), weight));
+            }
+        }
+
+        let mut builder: LabeledNetworkBuilder<String> = LabeledNetworkBuilder::new();
+        let labeled_network: LabeledNetwork<String> =
+            builder.build(edges.into_iter(), use_modularity);
+
+        return Ok(labeled_network);
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::super::compact_network::CompactNeighborItem;
+    use super::*;
+    use std::iter::FromIterator;
+
+    /*
+       same graph as the compact network graph, but our ids will be different based on order seen in edge list
+       edges
+       a, b, 2.0
+       a, d, 1.0
+       a, e, 1.0
+       b, a, 2.0
+       b, c, 6.0
+       b, e, 1.0
+       b, f, 4.0
+       b, g, 3.0
+       c, b, 6.0
+       c, g, 3.0
+       d, a, 1.0
+       d, h, 11.0
+       e, a, 1.0
+       e, b, 1.0
+       f, b, 4.0
+       g, b, 3.0
+       g, c, 3.0
+       h, d, 11.0
+    */
+
+    fn edge_list() -> Vec<Edge> {
+        let edges: Vec<Edge> = vec![
+            ("a".into(), "b".into(), 2.0),
+            ("a".into(), "d".into(), 1.0),
+            ("a".into(), "e".into(), 1.0),
+            ("b".into(), "a".into(), 2.0),
+            ("b".into(), "c".into(), 6.0),
+            ("b".into(), "e".into(), 1.0),
+            ("b".into(), "f".into(), 4.0),
+            ("b".into(), "g".into(), 3.0),
+            ("c".into(), "b".into(), 6.0),
+            ("c".into(), "g".into(), 3.0),
+            ("d".into(), "a".into(), 1.0),
+            ("d".into(), "h".into(), 11.0),
+            ("e".into(), "a".into(), 1.0),
+            ("e".into(), "b".into(), 1.0),
+            ("f".into(), "b".into(), 4.0),
+            ("g".into(), "b".into(), 3.0),
+            ("g".into(), "c".into(), 3.0),
+            ("h".into(), "d".into(), 11.0),
+        ];
+        return edges;
+    }
+
+    fn expected_label_mappings() -> (HashMap<String, usize>, Vec<String>) {
+        let label_order: Vec<String> = vec![
+            "a".into(),
+            "b".into(),
+            "d".into(),
+            "e".into(),
+            "c".into(),
+            "f".into(),
+            "g".into(),
+            "h".into(),
+        ];
+        let label_to_id_vec: Vec<(String, usize)> = label_order
+            .clone()
+            .into_iter()
+            .enumerate()
+            .map(|(index, label)| (label, index))
+            .collect();
+        let label_to_id: HashMap<String, usize> = HashMap::from_iter(label_to_id_vec.into_iter());
+        return (label_to_id, label_order);
+    }
+
+    #[test]
+    fn test_from_modularity() {
+        let edges = edge_list();
+        let mut builder: LabeledNetworkBuilder<String> = LabeledNetworkBuilder::new();
+        let labeled_network: LabeledNetwork<String> = builder.build(edges.into_iter(), true);
+        let (expected_label_map, expected_label_order) = expected_label_mappings();
+        assert_eq!(expected_label_order, labeled_network.id_to_labels);
+        assert_eq!(expected_label_map, labeled_network.labels_to_id);
+        // spot check
+        let b: usize = *labeled_network.labels_to_id.get("b").unwrap();
+        let a: usize = *labeled_network.labels_to_id.get("a").unwrap();
+        let c: usize = *labeled_network.labels_to_id.get("c").unwrap();
+        let e: usize = *labeled_network.labels_to_id.get("e").unwrap();
+        let f: usize = *labeled_network.labels_to_id.get("f").unwrap();
+        let g: usize = *labeled_network.labels_to_id.get("g").unwrap();
+        let expected_neighbors: Vec<(usize, f64)> = vec![
+            (a, 2.0),
+            (e, 1.0), // `e` appears before `c` in the edge list given above, due to its relationship with `a`. it thus gets sorted lower into the neighbors array due to its smaller CompactNodeId
+            (c, 6.0),
+            (f, 4.0),
+            (g, 3.0),
+        ];
+        let node = labeled_network.network_structure.node(b);
+        assert_eq!(16.0, node.weight);
+        let actual_neighbors: Vec<(usize, f64)> = node
+            .neighbors()
+            .map(
+                |CompactNeighborItem {
+                     id, edge_weight, ..
+                 }| { (id, edge_weight) },
+            )
+            .collect();
+        assert_eq!(expected_neighbors, actual_neighbors);
+    }
+
+    #[test]
+    fn test_from_cpm() {
+        let edges = edge_list();
+        let mut builder: LabeledNetworkBuilder<String> = LabeledNetworkBuilder::new();
+        let labeled_network: LabeledNetwork<String> = builder.build(edges.into_iter(), false);
+        let (expected_label_map, expected_label_order) = expected_label_mappings();
+        assert_eq!(expected_label_order, labeled_network.id_to_labels);
+        assert_eq!(expected_label_map, labeled_network.labels_to_id);
+        let b: usize = *labeled_network.labels_to_id.get("b").unwrap();
+        assert_eq!(1.0, labeled_network.network_structure.node(b).weight);
+    }
+}
diff --git a/packages/network_partitions/src/network/mod.rs b/packages/network_partitions/src/network/mod.rs
index 067b2e8..a853d7b 100644
--- a/packages/network_partitions/src/network/mod.rs
+++ b/packages/network_partitions/src/network/mod.rs
@@ -1,8 +1,20 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT license.
 
-pub use self::network::Network;
-pub use self::network_builder::NetworkBuilder;
+pub type Edge = (String, String, f64);
 
-mod network;
-mod network_builder;
+// pub use self::network::Network;
+// pub use self::network_builder::NetworkBuilder;
+
+pub use self::compact_network::{
+    ClusterId, CompactNetwork, CompactNodeId, CompactNodeItem, CompactSubnetworkItem,
+};
+pub use self::identifier::Identifier;
+pub use self::labeled_network::{LabeledNetwork, LabeledNetworkBuilder};
+pub use self::networks::NetworkDetails;
+
+mod compact_network;
+mod identifier;
+mod labeled_network;
+mod networks;
+pub mod prelude;
diff --git a/packages/network_partitions/src/network/networks.rs b/packages/network_partitions/src/network/networks.rs
new file mode 100644
index 0000000..5145b5d
--- /dev/null
+++ b/packages/network_partitions/src/network/networks.rs
@@ -0,0 +1,11 @@
+pub trait NetworkDetails {
+    fn num_nodes(&self) -> usize;
+
+    fn num_edges(&self) -> usize;
+
+    fn total_node_weight(&self) -> f64;
+
+    fn total_edge_weight(&self) -> f64;
+
+    fn total_self_links_edge_weight(&self) -> f64;
+}
diff --git a/packages/network_partitions/src/network/prelude.rs b/packages/network_partitions/src/network/prelude.rs
new file mode 100644
index 0000000..904b509
--- /dev/null
+++ b/packages/network_partitions/src/network/prelude.rs
@@ -0,0 +1,6 @@
+pub use super::compact_network::{
+    ClusterId, CompactNetwork, CompactNodeId, CompactNodeItem, CompactSubnetworkItem,
+};
+pub use super::labeled_network::{LabeledNetwork, LabeledNetworkBuilder};
+pub use super::networks::NetworkDetails;
+pub use super::Edge;
diff --git a/packages/network_partitions/src/quality.rs b/packages/network_partitions/src/quality.rs
index 846ab36..be1e7fb 100644
--- a/packages/network_partitions/src/quality.rs
+++ b/packages/network_partitions/src/quality.rs
@@ -4,10 +4,10 @@
 use super::resolution::adjust_resolution;
 use crate::clustering::Clustering;
 use crate::errors::CoreError;
-use crate::network::Network;
+use crate::network::prelude::*;
 
 pub fn quality(
-    network: &Network,
+    network: &CompactNetwork,
     clustering: &Clustering,
     resolution: Option<f64>,
     use_modularity: bool,
@@ -16,32 +16,24 @@ pub fn quality(
 
     let mut quality: f64 = 0_f64;
 
-    for node_index in 0..network.num_nodes() {
-        let node_cluster: usize = clustering.cluster_at(node_index)?;
-        let (start_neighbor_node_index, end_neighbor_node_index) =
-            network.neighbor_range(node_index)?;
-        for neighbor_node_index in start_neighbor_node_index..end_neighbor_node_index {
-            let neighbor_node: usize = network.neighbor_at(neighbor_node_index)?;
-            let neighbor_weight: f64 = network.weight_at(neighbor_node_index)?;
-            let neighbor_cluster: usize = clustering.cluster_at(neighbor_node)?;
+    let mut cluster_weights: Vec<f64> = vec![0_f64; clustering.next_cluster_id()];
+
+    for node in network {
+        let node_cluster: usize = clustering.cluster_at(node.id)?;
+        cluster_weights[node_cluster] += node.weight;
+        for neighbor in node.neighbors() {
+            let neighbor_cluster: usize = clustering.cluster_at(neighbor.id)?;
             if neighbor_cluster == node_cluster {
-                quality += neighbor_weight;
+                quality += neighbor.edge_weight;
             }
         }
     }
 
-    let mut cluster_weights: Vec<f64> = vec![0_f64; clustering.next_cluster_id()];
-    for node_index in 0..network.num_nodes() {
-        let cluster: usize = clustering.cluster_at(node_index)?;
-        cluster_weights[cluster] += network.node_weight_at(node_index)?;
-    }
-
     for cluster_weight in cluster_weights {
         quality -= cluster_weight.powi(2) * adjusted_resolution;
     }
 
-    quality =
-        quality / (2_f64 * network.total_edge_weight() + network.total_edge_weight_self_links());
+    quality /= 2_f64 * network.total_edge_weight() + network.total_self_links_edge_weight();
 
     return Ok(quality);
 }
diff --git a/packages/network_partitions/src/resolution.rs b/packages/network_partitions/src/resolution.rs
index f7d1436..7168c5c 100644
--- a/packages/network_partitions/src/resolution.rs
+++ b/packages/network_partitions/src/resolution.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT license.
 
-use crate::network::Network;
+use crate::network::prelude::*;
 
 pub const DEFAULT_RESOLUTION: f64 = 1_f64;
 
@@ -16,7 +16,7 @@ pub const DEFAULT_RESOLUTION: f64 = 1_f64;
 /// In either case, if the user doesn't specify a resolution, we use a default of 1.0 (though it may be scaled for modularity)
 pub fn adjust_resolution(
     resolution: Option<f64>,
-    network: &Network,
+    network: &CompactNetwork,
     use_modularity: bool,
 ) -> f64 {
     let resolution: f64 = resolution.unwrap_or(DEFAULT_RESOLUTION);
@@ -26,7 +26,7 @@ pub fn adjust_resolution(
         // which seems to be a bug since this resolution factor when used for modularity is
         // `resolution / 2m`, where m is the total edge weights of the graph.
         resolution
-            / (2_f64 * (network.total_edge_weight() + network.total_edge_weight_self_links()))
+            / (2_f64 * (network.total_edge_weight() + network.total_self_links_edge_weight()))
     } else {
         resolution
     };
diff --git a/packages/pyo3/Cargo.toml b/packages/pyo3/Cargo.toml
index 8d7eb71..e2e10ca 100644
--- a/packages/pyo3/Cargo.toml
+++ b/packages/pyo3/Cargo.toml
@@ -1,17 +1,25 @@
 [package]
-name = "topologic_native"
+name = "graspologic_native"
 version = "0.1.0"
-authors = ["Dwayne Pryce <dwpryce@microsoft.com>"]
+authors = ["dwpryce@microsoft.com"]
 edition = "2018"
 license = "MIT"
-description = ""
+description = "Python native companion module to the graspologic library"
+readme = "README.md"
+
+[package.metadata.maturin]
+maintainer = "Dwayne Pryce"
+maintainer-email = "dwpryce@microsoft.com"
+requires-python = ">=3.6,<=3.9"
+project-url = ["Github, https://github.com/microsoft/graspologic-native", "Graspologic, https://github.com/microsoft/graspologic"]
+classifier = ["Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Mathematics"]
 
 [lib]
-name = "topologic_native"
+name = "graspologic_native"
 crate-type = ["rlib","cdylib"]
 
 [dependencies]
-pyo3 = "0.8.5"
+pyo3 = "0.11.1"
 rand = "0.7"
 rand_xorshift = "0.2"
 network_partitions = { path = "../network_partitions" }
diff --git a/packages/pyo3/README.md b/packages/pyo3/README.md
new file mode 100644
index 0000000..05e0a14
--- /dev/null
+++ b/packages/pyo3/README.md
@@ -0,0 +1,24 @@
+# graspologic-native
+
+`graspologic-native` is a companion library to `graspologic`.  This module is a Python native module created by using
+the `network_partitions` crate from the same repository.
+
+The purpose of this module is to provide a faster implementations of graph/network analysis algorithms in a native
+without trying to work through the troubles of releasing Rust crates and Python modules at the same time (in specific 
+as the Python `graspologic` module is expected to be far more active than the Rust crates or native modules are).
+
+The only capability currently implemented by this module is the Leiden algorithm, described in the paper
+[From Louvain to Leiden: guaranteeing well-connected communities](https://openaccess.leidenuniv.nl/handle/1887/78029), 
+Traag, V.A.; Waltman, L.; Van, Eck N.J., Scientific Reports, Vol. 9, 2019.  In addition to the paper, the reference 
+implementation provided at [https://github.com/CWTSLeiden/networkanalysis](https://github.com/CWTSLeiden/networkanalysis)
+was used as a starting point.
+
+## Releases
+Builds are provided for x86_64 architectures only, for Windows, macOS, and Linux, for Python versions 3.6->3.9.
+
+## Build Tools
+Rust nightly 1.37+ (we are currently using 1.40)
+The python package [maturin](https://github.com/pyo3/maturin)
+
+Please consider using [graspologic](https://github.com/microsoft/graspologic) in lieu of `graspologic-native`, as the 
+former will contain some nice wrappers to make usage of this library more pythonic.
diff --git a/packages/pyo3/src/lib.rs b/packages/pyo3/src/lib.rs
index 4c743da..d07d2a5 100644
--- a/packages/pyo3/src/lib.rs
+++ b/packages/pyo3/src/lib.rs
@@ -10,22 +10,56 @@ use std::collections::{HashMap, HashSet};
 
 use pyo3::exceptions::{TypeError, ValueError};
 use pyo3::prelude::*;
-use pyo3::{create_exception, wrap_pyfunction, wrap_pymodule};
+use pyo3::{create_exception, wrap_pyfunction, wrap_pymodule, PyObjectProtocol};
 
-use pyo3::type_object::initialize_type;
 use pyo3::types::{PyDict, PyInt, PyList, PyString, PyTuple};
 
 use network_partitions::clustering::Clustering;
 use network_partitions::errors::CoreError;
-use network_partitions::leiden::leiden as leiden_internal;
 use network_partitions::log;
-use network_partitions::network::{Network, NetworkBuilder};
+use network_partitions::network::prelude::*;
 use network_partitions::quality;
 
 use network_partitions::safe_vectors::SafeVectors;
 
 use errors::*;
 
+#[pyclass]
+pub struct HierarchicalCluster {
+    #[pyo3(get)]
+    node: String,
+    #[pyo3(get)]
+    cluster: ClusterId,
+    #[pyo3(get)]
+    level: u32,
+    #[pyo3(get)]
+    parent_cluster: Option<ClusterId>,
+    #[pyo3(get)]
+    is_final_cluster: bool,
+}
+
+#[pyproto]
+impl PyObjectProtocol for HierarchicalCluster {
+    fn __repr__(&self) -> PyResult<String> {
+        let parent: String = self
+            .parent_cluster
+            .map(|level| level.to_string())
+            .unwrap_or("None".into());
+        return Ok(format!(
+            "HierarchicalCluster(node=\"{}\", cluster=\"{}\", level={}, parent_cluster={}, is_final_cluster={})",
+            self.node,
+            self.cluster,
+            self.level,
+            parent,
+            self.is_final_cluster,
+        ));
+    }
+
+    fn __str__(&self) -> PyResult<String> {
+        return self.__repr__();
+    }
+}
+
 #[pyfunction(
     "*",
     resolution = "1.0",
@@ -33,15 +67,17 @@ use errors::*;
     iterations = "1",
     use_modularity = "true"
 )]
-#[text_signature = "(edges, /starting_communities, resolution, randomness, iterations, use_modularity, seed)"]
-/// Leiden is a community detection algorithm based on the Louvain algorithm.
+#[text_signature = "(edges, /, starting_communities, resolution, randomness, iterations, use_modularity, seed)"]
+/// Leiden is a global network partitioning algorithm. Given a list of edges and a maximization
+/// function, it will iterate through the network attempting to find an optimal partitioning of
+/// the entire network.
 ///
-/// :param edges: A list of edges.
+/// :param edges: A list of edges, defined with the source and target encoded as strings and the edge weight being a float.
 /// :type edges: List[Tuple[str, str, float]]
 /// :param starting_communities: An optional initial mapping of nodes to their community. Note that
 ///     this function does require that all nodes in the edge list have a community and nodes in the
 ///     community dictionary exist as a node in the provided edge list. The community values must
-///     also be a non negative number.
+///     also be a non negative integer.
 /// :type starting_communities: Optional[Dict[str, int]]
 /// :param float resolution: Default is `1.0`. Higher resolution values lead to more communities and
 ///     lower resolution values leads to fewer communities. Must be greater than 0.
@@ -69,7 +105,7 @@ use errors::*;
 /// :raises UnsafeInducementError: An internal algorithm error. Please report with reproduction steps.
 fn leiden(
     py: Python,
-    edges: Vec<(String, String, f64)>,
+    edges: Vec<Edge>,
     starting_communities: Option<HashMap<String, usize>>,
     resolution: f64,
     randomness: f64,
@@ -99,11 +135,118 @@ fn leiden(
     return result.map_err(|err| PyErr::from(err));
 }
 
+#[pyfunction(
+    "*",
+    resolution = "1.0",
+    randomness = "0.001",
+    iterations = "1",
+    use_modularity = "true",
+    max_cluster_size = "1000"
+)]
+#[text_signature = "(edges, /, starting_communities, resolution, randomness, iterations, use_modularity, max_cluster_size, seed)"]
+/// Hierarchical leiden builds upon the leiden function by further breaking down exceptionally large clusters.
+///
+/// The process followed is to run leiden the first time, then each cluster with membership
+/// counts >= `max_cluster_size` (default 1000) are isolated and turned into a subnetwork, which
+/// then has the leiden process run over it. The resulting clusters are merged into the original
+/// clustering as new clusters, the original cluster ID will be left without any nodes belonging to
+/// it.  This is done for each subnetwork, and it is done iteratively until no cluster contains more
+/// than `max_cluster_size` entries.
+///
+/// The results are different from the regular hierarchical leiden as well.  A List of the `HierarchicalCluster`
+/// items is returned.  This HierarchicalCluster describes a node->cluster relationship, by level, and also contains
+/// a link back to the parent/previous cluster, and a flag denoting whether it is the final clustering
+/// for a given node or not.
+///
+/// This hierarchical structure allows us to navigate our clusterings by breaking down truly large
+/// clusters into smaller, fine grained clusters, but still be able to see the larger clustered structure.
+///
+/// :param edges: A list of edges, defined with the source and target encoded as strings and the edge weight being a float.
+/// :type edges: List[Tuple[str, str, float]]
+/// :param starting_communities: An optional initial mapping of nodes to their community. Note that
+///     this function does require that all nodes in the edge list have a community and nodes in the
+///     community dictionary exist as a node in the provided edge list. The community values must
+///     also be a non negative integer.
+/// :type starting_communities: Optional[Dict[str, int]]
+/// :param float resolution: Default is `1.0`. Higher resolution values lead to more communities and
+///     lower resolution values leads to fewer communities. Must be greater than 0.
+/// :param float randomness: Default is `0.001`. The larger the randomness value, the more
+///     exploration of the partition space is possible. This is a major difference from the Louvain
+///     algorithm. The Louvain algorithm is purely greedy in the partition exploration.
+/// :param int iterations: Default is `1`. The leiden algorithm is recursive, but subject to pseudo-random
+///     number generators which sometimes lead to suboptimal community membership. Setting a number
+///     greater than 1 will force leiden to run at minimum `iterations - 1` more times seeking a
+///     more optimal partitioning.
+/// :param bool use_modularity: Default is `True`. Whether to use modularity or CPM as the
+///     maximization function.
+/// :param int max_cluster_size: Default is `1000`. Any cluster larger than 1000 will be broken into
+///     an isolated subnetwork and have leiden run over it for further refinement.
+/// :param Optional[int] seed: Default is `None`. If provided, the seed will be used in creating the
+///     Pseudo-Random Number Generator at a known state, making runs over the same network and
+///     starting_communities with the same parameters end with the same results.
+/// :return: A list of HierarchicalCluster entries. A hierarchical cluster contains a node id, the
+///     cluster id, the level, an optional parent, and whether or not it is the final entry for that
+///     node.
+/// :rtype: List[HierarchicalCluster]
+/// :raises ClusterIndexingError:
+/// :raises EmptyNetworkError:
+/// :raises InvalidCommunityMappingError: The starting communities dictionary either did not include
+///     a community for each node in the edge list or the edge list did not contain a node present
+///     in the starting community mapping.
+/// :raises InternalNetworkIndexingError: An internal algorithm error. Please report with reproduction steps.
+/// :raises ParameterRangeError: One of the parameters provided did not meet the requirements in the documentation.
+/// :raises UnsafeInducementError: An internal algorithm error. Please report with reproduction steps.
+fn hierarchical_leiden(
+    py: Python,
+    edges: Vec<Edge>,
+    starting_communities: Option<HashMap<String, usize>>,
+    resolution: f64,
+    randomness: f64,
+    iterations: usize,
+    use_modularity: bool,
+    max_cluster_size: u32,
+    seed: Option<u64>,
+) -> PyResult<Vec<HierarchicalCluster>> {
+    #[cfg(feature = "logging")]
+    use std::time::Instant;
+    #[cfg(feature = "logging")]
+    let now: Instant = Instant::now();
+
+    log!("pyo3 converted {} edges from Python's representation to a Vec<(String, String, f64)> representation at {:?}", edges.len(), now);
+
+    let result: Result<Vec<HierarchicalCluster>, PyLeidenError> = py.allow_threads(move || {
+        mediator::hierarchical_leiden(
+            edges,
+            starting_communities,
+            resolution,
+            randomness,
+            iterations,
+            use_modularity,
+            max_cluster_size,
+            seed,
+        )
+    });
+    return result.map_err(|err| PyErr::from(err));
+}
+
 #[pyfunction("*", resolution = "1.0")]
-#[text_signature = "(edges, communities, /resolution)"]
+#[text_signature = "(edges, communities, /, resolution)"]
+/// Measures the modularity for a global partitioning of a network described by a list of edges.
+///
+/// :param edges: A list of edges, defined with the source and target encoded as strings and the edge weight being a float.
+/// :type edges: List[Tuple[str, str, float]]
+/// :param communities: An optional initial mapping of nodes to their community. Note that
+///     this function does require that all nodes in the edge list have a community and nodes in the
+///     community dictionary exist as a node in the provided edge list. The community values must
+///     also be a non negative number.
+/// :type communities: Dict[str, int]
+/// :param float resolution: Default is `1.0`. Higher resolution values lead to more communities and
+///     lower resolution values leads to fewer communities. Must be greater than 0.
+/// :return: The modularity of the community partitioning provided for the network.
+/// :rtype: float
 fn modularity(
     py: Python,
-    edges: Vec<(String, String, f64)>,
+    edges: Vec<Edge>,
     communities: HashMap<String, usize>,
     resolution: f64,
 ) -> PyResult<f64> {
@@ -113,14 +256,16 @@ fn modularity(
     return result.map_err(|err| PyErr::from(err));
 }
 
-/// topologic_native currently supports global network partitioning via the Leiden University
+/// graspologic_native currently supports global network partitioning via the Leiden University
 /// algorithm described by https://arxiv.org/abs/1810.08473
 #[pymodule]
-fn topologic_native(
+fn graspologic_native(
     py: Python,
     module: &PyModule,
 ) -> PyResult<()> {
+    module.add_class::<HierarchicalCluster>()?;
     module.add_wrapped(wrap_pyfunction!(leiden))?;
+    module.add_wrapped(wrap_pyfunction!(hierarchical_leiden))?;
     module.add_wrapped(wrap_pyfunction!(modularity))?;
 
     module.add(
@@ -144,16 +289,3 @@ fn topologic_native(
     module.add("QueueError", py.get_type::<QueueError>())?;
     Ok(())
 }
-
-#[cfg(test)]
-mod tests {
-
-    #[test]
-    fn test_starting_communities_to_clustering() {
-        // test None
-        // test empty
-        // test gapped
-        // test a mapping that includes nodes that aren't in the network
-        // test a mapping that doesn't include all the nodes in the network
-    }
-}
diff --git a/packages/pyo3/src/mediator.rs b/packages/pyo3/src/mediator.rs
index 3073230..9624ad1 100644
--- a/packages/pyo3/src/mediator.rs
+++ b/packages/pyo3/src/mediator.rs
@@ -5,19 +5,20 @@ use std::collections::{HashMap, HashSet};
 
 use network_partitions::clustering::Clustering;
 use network_partitions::errors::CoreError;
-use network_partitions::leiden::leiden as leiden_internal;
+use network_partitions::leiden;
 use network_partitions::log;
-use network_partitions::network::{Network, NetworkBuilder};
+use network_partitions::network::prelude::*;
 use network_partitions::quality;
 use network_partitions::safe_vectors::SafeVectors;
 
+use super::errors::PyLeidenError;
+use super::HierarchicalCluster;
+use crate::errors::InvalidCommunityMappingError;
 use rand::{Rng, SeedableRng};
 use rand_xorshift::XorShiftRng;
 
-use super::errors::PyLeidenError;
-
 pub fn leiden(
-    edges: Vec<(String, String, f64)>,
+    edges: Vec<Edge>,
     starting_communities: Option<HashMap<String, usize>>,
     resolution: f64,
     randomness: f64,
@@ -26,21 +27,20 @@ pub fn leiden(
     seed: Option<u64>,
 ) -> Result<(bool, f64, HashMap<String, usize>), PyLeidenError> {
     log!(
-        "Building a NetworkBuilder for a Network whose clustering is measured by {}",
+        "Building a LabeledNetwork for quality measured by {}",
         if use_modularity { "modularity" } else { "CPM" }
     );
-    let builder: NetworkBuilder = NetworkBuilder::builder(use_modularity);
     log!("Adding {} edges to network builder", edges.len());
-    let builder: NetworkBuilder = builder.from(edges);
 
-    log!("Loaded List[(str, str, f64)] into network builder");
-    let network: Network = builder.build();
+    let mut builder: LabeledNetworkBuilder<String> = LabeledNetworkBuilder::new();
+    let labeled_network: LabeledNetwork<String> = builder.build(edges.into_iter(), use_modularity);
 
-    log!("Network built from NetworkBuilder");
+    log!("Network built from edges");
     let clustering: Option<Clustering> = match starting_communities {
-        Some(starting_communities) => {
-            Some(communities_to_clustering(&network, starting_communities)?)
-        }
+        Some(starting_communities) => Some(communities_to_clustering(
+            &labeled_network,
+            starting_communities,
+        )?),
         None => None,
     };
 
@@ -51,8 +51,10 @@ pub fn leiden(
         None => XorShiftRng::from_entropy(),
     };
 
-    let (improved, clustering) = leiden_internal(
-        &network,
+    let compact_network: &CompactNetwork = labeled_network.compact();
+
+    let (improved, clustering) = leiden::leiden(
+        compact_network,
         clustering,
         Some(iterations),
         Some(resolution),
@@ -62,11 +64,15 @@ pub fn leiden(
     )?;
 
     log!("Completed leiden process");
-    let quality_score: f64 =
-        quality::quality(&network, &clustering, Some(resolution), use_modularity)?;
+    let quality_score: f64 = quality::quality(
+        compact_network,
+        &clustering,
+        Some(resolution),
+        use_modularity,
+    )?;
 
     log!("Calculated quality score");
-    let clustering: HashMap<String, usize> = map_from(&network, &clustering)?;
+    let clustering: HashMap<String, usize> = map_from(&labeled_network, &clustering)?;
 
     log!("Mapped the clustering back to a dictionary: {:?}");
 
@@ -74,61 +80,127 @@ pub fn leiden(
 }
 
 pub fn modularity(
-    edges: Vec<(String, String, f64)>,
+    edges: Vec<Edge>,
     communities: HashMap<String, usize>,
     resolution: f64,
 ) -> Result<f64, PyLeidenError> {
-    let builder: NetworkBuilder = NetworkBuilder::for_modularity();
-    let network: Network = builder.from(edges).build();
-    let clustering: Clustering = communities_to_clustering(&network, communities)?;
-    let quality: f64 = quality::quality(&network, &clustering, Some(resolution), true)?;
+    let mut builder: LabeledNetworkBuilder<String> = LabeledNetworkBuilder::new();
+    let labeled_network: LabeledNetwork<String> = builder.build(edges.into_iter(), true);
+    let clustering: Clustering = communities_to_clustering(&labeled_network, communities)?;
+    let quality: f64 = quality::quality(
+        labeled_network.compact(),
+        &clustering,
+        Some(resolution),
+        true,
+    )?;
     return Ok(quality);
 }
 
+pub fn hierarchical_leiden(
+    edges: Vec<Edge>,
+    starting_communities: Option<HashMap<String, usize>>,
+    resolution: f64,
+    randomness: f64,
+    iterations: usize,
+    use_modularity: bool,
+    max_cluster_size: u32,
+    seed: Option<u64>,
+) -> Result<Vec<HierarchicalCluster>, PyLeidenError> {
+    log!(
+        "Building a LabeledNetwork for quality measured by {}",
+        if use_modularity { "modularity" } else { "CPM" }
+    );
+    log!("Adding {} edges to network builder", edges.len());
+
+    let mut builder: LabeledNetworkBuilder<String> = LabeledNetworkBuilder::new();
+    let labeled_network: LabeledNetwork<String> = builder.build(edges.into_iter(), use_modularity);
+
+    log!("Network built from edges");
+    let clustering: Option<Clustering> = match starting_communities {
+        Some(starting_communities) => Some(communities_to_clustering(
+            &labeled_network,
+            starting_communities,
+        )?),
+        None => None,
+    };
+
+    log!("Mapped any starting communities from a dictionary into a clustering");
+
+    let mut rng: XorShiftRng = match seed {
+        Some(seed) => XorShiftRng::seed_from_u64(seed),
+        None => XorShiftRng::from_entropy(),
+    };
+
+    log!("Running hierarchical leiden over a network of {} nodes, {} edges, with a max_cluster_size of {}", labeled_network.num_nodes(), labeled_network.num_edges(), max_cluster_size);
+    let compact_network: &CompactNetwork = labeled_network.compact();
+    let internal_clusterings: Vec<leiden::HierarchicalCluster> = leiden::hierarchical_leiden(
+        compact_network,
+        clustering,
+        Some(iterations),
+        Some(resolution),
+        Some(randomness),
+        &mut rng,
+        use_modularity,
+        max_cluster_size,
+    )?;
+
+    log!("Completed hierarchical leiden process");
+
+    let mut hierarchical_clustering: Vec<HierarchicalCluster> =
+        Vec::with_capacity(internal_clusterings.len());
+    for internal in internal_clusterings {
+        let node = labeled_network.label_for(internal.node);
+        hierarchical_clustering.push(HierarchicalCluster {
+            node: node.into(),
+            cluster: internal.cluster,
+            level: internal.level,
+            parent_cluster: internal.parent_cluster,
+            is_final_cluster: internal.is_final_cluster,
+        });
+    }
+
+    return Ok(hierarchical_clustering);
+}
+
 fn map_from(
-    network: &Network,
+    network: &LabeledNetwork<String>,
     clustering: &Clustering,
 ) -> Result<HashMap<String, usize>, CoreError> {
     let mut map: HashMap<String, usize> = HashMap::with_capacity(clustering.num_nodes());
-    for node_index in 0..clustering.num_nodes() {
-        let cluster: usize = clustering.cluster_at(node_index)?;
-        let node_name = network.node_name(node_index)?;
-        map.insert(node_name, cluster);
+    for item in clustering {
+        let node_name = network.label_for(item.node_id);
+        map.insert(node_name.into(), item.cluster);
     }
     return Ok(map);
 }
 
 fn communities_to_clustering(
-    network: &Network,
+    network: &LabeledNetwork<String>,
     communities: HashMap<String, usize>,
 ) -> Result<Clustering, PyLeidenError> {
     // if node not found in internal network, bounce
     // if max(communities) > len(set(communities)), collapse result
     // if all nodes are mapped, cool
     // if not all nodes are mapped, generate integers for each new value
-    let mut max_community: usize = 0;
+
+    let mut clustering: Clustering = Clustering::as_self_clusters(network.num_nodes());
+
     let mut all_communities: HashSet<usize> = HashSet::new();
-    let mut node_to_community: Vec<usize> = vec![0; communities.len()];
+
     for (node, community) in communities {
         all_communities.insert(community);
-        max_community = max_community.max(community);
-        let mapped_node: usize = network
-            .index_for_name(&node)
+        let mapped_node: CompactNodeId = network
+            .compact_id_for(node)
             .ok_or(PyLeidenError::InvalidCommunityMappingError)?;
-        node_to_community[mapped_node] = community;
-    }
-    // let's validate that all the nodes in the internal network have an entry in the
-    // starting communities
-    for node_id in 0..network.num_nodes() {
-        node_to_community
-            .get_or_err(node_id, CoreError::ClusterIndexingError)
+        clustering
+            .update_cluster_at(mapped_node, community)
             .map_err(|_| PyLeidenError::InvalidCommunityMappingError)?;
     }
 
-    let mut clustering: Clustering = Clustering::as_defined(node_to_community, max_community + 1);
     if clustering.next_cluster_id() != all_communities.len() {
         // we have some gaps, compress
         clustering.remove_empty_clusters();
     }
+
     return Ok(clustering);
 }


Test patch:
diff --git a/packages/network_partitions/tests/test_network.rs b/packages/network_partitions/tests/test_network.rs
index c6966cd..8e2164c 100644
--- a/packages/network_partitions/tests/test_network.rs
+++ b/packages/network_partitions/tests/test_network.rs
@@ -4,22 +4,27 @@
 #[cfg(test)]
 mod tests {
     use network_partitions::errors::NetworkError;
-    use network_partitions::network::NetworkBuilder;
-    use std::collections::HashSet;
+    use network_partitions::network::prelude::*;
 
     #[test]
     fn test_load_network_from_file() {
         let org_network_path = "tests/simple_org_graph.csv";
         let broken_network_path = "tests/broken_org_graph.csv";
-        let builder = NetworkBuilder::for_modularity()
-            .load_from_file(org_network_path, ",", 0, 1, Some(2), false)
-            .expect("Could not load from file");
-        assert_eq!(10, builder.num_nodes());
-        assert_eq!(15.0, builder.get_edge_weight("david", "amber").unwrap());
-
-        let builder = NetworkBuilder::builder(true);
-        let result: Result<NetworkBuilder, NetworkError> =
-            builder.load_from_file(broken_network_path, ",", 0, 1, Some(2), false);
+        let labeled_network: LabeledNetwork<String> =
+            LabeledNetwork::<String>::load_from(org_network_path, ",", 0, 1, Some(2), false, true)
+                .expect("We should have gotten a properly loaded labeled network from this");
+        assert_eq!(10, labeled_network.num_nodes());
+
+        let result: Result<LabeledNetwork<String>, NetworkError> =
+            LabeledNetwork::<String>::load_from(
+                broken_network_path,
+                ",",
+                0,
+                1,
+                Some(2),
+                false,
+                true,
+            );
         assert!(result.is_err());
         match result.err() {
             Some(NetworkError::EdgeFileFormatError) => assert!(true),
@@ -34,51 +39,4 @@ mod tests {
             ),
         }
     }
-
-    #[test]
-    fn test_network_builder() {
-        let network_path = "tests/sbm_network.csv";
-        let builder = NetworkBuilder::for_modularity()
-            .load_from_file(network_path, ",", 0, 1, Some(2), false)
-            .expect("Could not load from file");
-
-        let cloned_builder = builder.clone();
-
-        let network = builder.build();
-
-        assert_eq!(cloned_builder.index_to_node.len(), network.num_nodes());
-        assert_eq!(
-            if cloned_builder.total_edge_weight_self_links == 0_f64 {
-                1_f64
-            } else {
-                cloned_builder.total_edge_weight_self_links
-            },
-            network.total_edge_weight_self_links()
-        );
-
-        for i in 0..network.num_nodes() {
-            // check node weight
-            assert_eq!(
-                cloned_builder.node_weights[i],
-                network
-                    .node_weight_at(i)
-                    .expect("node weight to exist here")
-            );
-            // generate neighbor list to check, but also check against the weights
-            let (start_range, end_range) = network.neighbor_range(i).expect("neighbor range");
-            let mut network_neighbors: HashSet<usize> = HashSet::new();
-            for neighbor_index in start_range..end_range {
-                let neighbor = network
-                    .neighbor_at(neighbor_index)
-                    .expect("Expected a neighbor index");
-                network_neighbors.insert(neighbor);
-
-                let weight = network
-                    .weight_at(neighbor_index)
-                    .expect("expected a weight");
-                assert_eq!(cloned_builder.edges.get(&(i, neighbor)), Some(&weight))
-            }
-            assert_eq!(cloned_builder.node_neighbors[i], network_neighbors);
-        }
-    }
 }