Skip to content

feat: implement the graph API and core algorithms - #3

Merged
srilman merged 3 commits into
daft-engine:mainfrom
nish2292:feat/core-api-and-algorithms
Sep 17, 2026
Merged

srilman merged 3 commits into
daft-engine:mainfrom
nish2292:feat/core-api-and-algorithms

Conversation

@nish2292

Copy link
Copy Markdown
Contributor

Implements the Discussion #1 API and the core algorithms requested in #2. The package was an empty template, so this provides the initial implementation.

API (the two-class model from #1)

  • Abstract Graph base with DirectedGraph and UndirectedGraph subclasses, so the type carries direction and each algorithm declares the flavor it needs.
  • Built from an edge list with optional vertices and configurable columns.
  • Graph methods: degrees, triplets, filter_vertices / filter_edges, drop_isolated_vertices, degree_by_type, and directed/undirected conversion (reverse, as_undirected, as_directed).

Algorithms (the #2 set)

  • Connected components: connected_components (weak), strongly_connected_components
  • Centrality: pagerank (+ personalized), parallel_personalized_pagerank
  • Traversal: bfs, bfs_paths, shortest_paths, all_shortest_paths, all_paths
  • Community: label_propagation, power_iteration_clustering
  • Motif: find (GraphFrames-style DSL)
  • Message passing: aggregate_messages, pregel
  • Also: triangle_count, k_core, cycle detection (has_cycle, vertices_on_cycles), maximal_independent_set, random_walks, svd_plus_plus, hyper_anf, id reindexing, and edge utilities

Implementation

  • Pure Python on top of Daft. Iterative algorithms run on a materialize-between-rounds engine, the analog of GraphFrames checkpointing.
  • Connected components ports the large-star / small-star contraction algorithm (Kiveris et al. 2014) from Daft's own minhash-dedupe example, then hardens it with types and tests.
  • The core depends on daft alone. numpy and scipy are an optional local extra used only for the single-node solves (connected_components strategy="local", svd_plus_plus).
  • Correctness is validated against igraph and networkx (test-only). 331 tests pass, ruff and mypy are clean, on Python 3.10 through 3.13.

Opening as a draft for maintainer feedback on scope and shape. Closes #2.

Implements the Discussion daft-engine#1 two class API and the core algorithm set from
issue daft-engine#2, ported from the validated internal prototype and shaped to this
repo's conventions (Apache-2.0, hatchling, ruff/pydocstyle, Python 3.10+).

API:
- abstract Graph plus DirectedGraph and UndirectedGraph; the type carries
  direction, so algorithms declare the flavor they need
- construction from edges with optional vertices, configurable columns,
  opt in validation; transforms preserve the concrete flavor

Algorithms:
- connected components (regular + strong), pagerank (+ personalized) and
  parallel_personalized_pagerank, bfs/bfs_paths/shortest_paths/
  all_shortest_paths/all_paths, label_propagation, power_iteration_clustering,
  find (motif DSL), aggregate_messages/pregel, triangle_count, k_core,
  cycle detection, maximal_independent_set, random_walks, svd_plus_plus,
  hyper_anf, plus reindex/restore_ids and edge utilities

Packaging:
- core depends on daft only; numpy and scipy behind the optional local extra
- Self imported under TYPE_CHECKING for the 3.10 target
- igraph and networkx are test only oracles

Verified: 331 tests pass, ruff and mypy clean.
Every distributed iterative algorithm stalled on Daft's Ray/Flotilla runner:
partition counts compounded round over round until each shuffle needed a
partition-count-squared number of pieces, exhausting the cluster. Daft resolves
a shuffle's output partition count to the repartition spec's count or else the
input's (unwrap_or(input_num_partitions)) and never lowers it, and union_all
sums its inputs' counts, so a step that symmetrizes/unions doubles the count
every round. Invisible on the native runner (no partitions), fatal on Ray.

Fix: bound the partition count wherever iterative state is carried, via a cheap
plan rewrite (into_partitions on the already-optimized plan, no extra execution),
gated to the Ray runner (no-op on native):

- iterate.py: bound_partitions() (lazy cap) and collect_bounded() (materialize
  then present at a bounded count). collect_bounded returns the coalesced frame
  LAZILY - re-collecting after into_partitions makes num_partitions() report 0,
  which silently disables every downstream cap.
- message_passing.py: cap triplets, the aggregate_messages union, and each
  pregel step (fixes label_propagation, k_core, shortest_paths, pagerank, ...).
- connected_components: cap the star step passes, label propagation, adjacency.
- Custom-loop algorithms that bypass the shared machinery: strongly_connected_
  components (peeling loop + active_v/active_e + union fold), hyper_anf (per-hop
  HLL), maximal_independent_set (per-round status), shortest_paths (landmark
  fold), and the shared BFS frontier in _traversal (visited/levels growth).
- Static once-collected inputs (adjacency/edges/degrees) joined every round.

Also adds tests/test_bfs_scaling.py, examples/, and benchmarks/ from the
driver-memory BFS rewrite.
@nish2292
nish2292 marked this pull request as ready for review August 23, 2026 05:51
@srilman
srilman self-requested a review September 17, 2026 21:08

@srilman srilman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the great stuff @nish2292! I have a couple of smaller changes I want to make, but it shouldn't block us from merging this. Thanks!

Comment thread daft_graph/__init__.py
"UndirectedGraph",
"__version__",
"aggregate_messages",
"all_paths",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it would be nice if a bunch of these functions are exposed as methods instead. That way, we can change the API per Undirected or Directed graph type?

Comment thread daft_graph/graph.py
Returns:
A :class:`DirectedGraph` over the same vertices and edges.
"""
return DirectedGraph(self._edges, self._vertices, src_col=src_col, dst_col=dst_col)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think when converting from an undirected to directed graph, the general convention is that each undirected edge becomes 2 directed edges, so we're effectively duplicating the number of edges. What do you think?

@srilman
srilman merged commit e7f02a3 into daft-engine:main Sep 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement basic API and core algorithsm

2 participants