Quickstart Guide

This page shows the two most useful starting points: the packaged load balancing workflow and the lower-level decomposition API. Start with load balancing when you need balanced graph communities. Drop to the decomposition API when you need to customize the column-generation pieces directly.

Load Balancing Run

import networkx as nx

from asunder.load_balancing import LoadBalancer


G = nx.Graph()
G.add_edges_from(
    [
        ("a", "b"),
        ("a", "c"),
        ("b", "c"),
        ("c", "d"),
        ("d", "e"),
        ("e", "f"),
        ("d", "f"),
    ]
)

result = LoadBalancer(
    G,
    K=2,
    R=1,
    must_link=[("a", "b")],
    cannot_link=[("a", "f")],
    disable_tqdm=True,
)

print(result.final_partition)
print(result.metadata["modularity"])

LoadBalancer accepts node labels from the input graph in must_link and cannot_link constraints, then returns a DecompositionResult whose partition and metadata map communities back to those labels. Use K and R for near-equal communities, or use R_bounds=(lower, upper) when every community must stay inside explicit size bounds.

Minimal Decomposition Run

import numpy as np

from asunder import CSDDecompositionConfig, run_csd_decomposition


def trivial_ifc_generator(N, **_):
    # One feasible starting column: all nodes placed in a single block.
    return [np.ones((N, N), dtype=int)]


A = np.array(
    [
        [0, 1, 1, 0],
        [1, 0, 1, 0],
        [1, 1, 0, 1],
        [0, 0, 1, 0],
    ],
    dtype=float,
)

cfg = CSDDecompositionConfig(
    ifc_params={
        "generator": trivial_ifc_generator,
        "num": 1,
        "args": {"N": A.shape[0]},
    },
    extract_dual=False,
    final_master_solve=False,
    max_iterations=2,
    verbose=0,
)

result = run_csd_decomposition(A, config=cfg)

print(result.metadata)
print(result.final_partition)

This example uses the top-level facade. Internally, the top-level API delegates to the reusable modules in asunder.base.

Important Practical Note

The decomposition loop expects an initial feasible column generator unless you provide an existing column pool. In many real applications, that generator is problem-specific and deserves deliberate design. The trivial example above is appropriate only for smoke tests and first experiments.

Using Canonical Namespaces

The canonical reusable imports live under asunder.base. For example:

from asunder.base.column_generation.subproblem import heuristic_subproblem
from asunder.base.algorithms.modular_VFD import modular_very_fortunate_descent

The generic nonlinear branch-and-price workflow and application pieces live under asunder.nlbnp:

from asunder.nlbnp import CorePeripheryPartition, NonlinearBranchAndPrice
from asunder.nlbnp.case_studies import build_circle_cutting_graph, run_evaluation
from asunder.nlbnp.algorithms.refinement import refine_partition_linear_group, refine_partition_with_cp

Use NonlinearBranchAndPrice when you already have a graph and want the NLBNP column-generation workflow without a packaged case-study builder:

import networkx as nx

from asunder.nlbnp import NonlinearBranchAndPrice


G = nx.Graph()
G.add_edge("u", "v", edge_kind="integer")
G.add_edge("v", "w", edge_kind="continuous")

result = NonlinearBranchAndPrice(
    G,
    worthy_edge_attr="edge_kind",
    worthy_edge_value="integer",
    algorithm="louvain",
    package="networkx",
    disable_tqdm=True,
)

print(result.final_partition)

Use CorePeripheryPartition when removing the detected core leaves connected periphery components that are already suitable final communities:

community_labels, metadata = CorePeripheryPartition(
    G,
    unworthy_edge_attr="edge_kind",
    unworthy_edge_value="continuous",
    cp_algorithm="SPEC",
)

print(metadata["community_map_labels"])

Use NonlinearBranchAndPrice when the community structure is beyond the direct core-periphery logic. Core-periphery detection can be used through the generic refine_params hook:

result = NonlinearBranchAndPrice(
    G,
    refine_params={
        "refine_func": refine_partition_with_cp,
        "kwargs": {
            "unworthy_edges": [(1, 2)],
            "nonlinear_nodes": [],
            "cp_algorithm": "SPEC",
        },
    },
    disable_tqdm=True,
)

The load balancing application pieces live under asunder.load_balancing:

from asunder.load_balancing import LoadBalancer
from asunder.load_balancing.utils.partition_generation import make_partitions

Built-In NLBNP Case-Study Evaluation

If you want to use the packaged nonlinear branch-and-price benchmark evaluation flow, the top-level convenience wrapper remains available:

from asunder import run_evaluation

results = run_evaluation(
    problem="cpcong",
    build_params={"K": 2, "J": 3, "T": 5},
    style="CP",
    algos=["SPEC"],
    repeat=1,
)

print(results["SPEC"])

Where To Go Next

  • For direct reusable APIs, see API -> Base API.

  • For balanced graph partitioning, see API -> Load Balancing API.

  • For the generic and case-study NLBNP workflows, see API -> NLBNP API.

  • For a fuller explanation of when the package works well as-is versus when you should customize it, see Problem Fit and Utility.