Changelog¶
All notable changes to this project are recorded here. The format follows Keep a Changelog, and versions follow Semantic Versioning.
The distribution is ursa-graph; the import name is ursa.
Unreleased¶
Fixed¶
scan_edgesandscan_nodesnow return rows in file order on every run. A file large enough to be read as several parallel partitions previously came back in a run-dependent order, so dense ids, component labels,edge_idsand PageRank's last bits changed between runs over the same file. Output over a scan now matchesfrom_arrowover the same table exactly (#148).scan_edgesandscan_nodesover a Parquet file with string ids failed with "Utf8View is not a supported id type". DataFusion reads Parquet strings asUtf8View; string ids are now canonicalized toUtf8likeLargeUtf8. CSV and in-memory string ids were not affected.- A kernel that asked for the lazily built transpose or undirected view from
inside a parallel loop could deadlock, e.g.
neighbors().agg()orhopwithdirection="in"/"both"on graphs above 65,536 edges.
Changed¶
- Building the graph index over an unweighted
scan_edgessource no longer holds the scanned Arrow endpoints for the whole build. Each batch is interned and freed as it is decoded, inside one native call, so the 16 B/edge input never accumulates. On a 30M-edge Parquet file the build's peak memory fell from about 45 to 24 B/edge, and the footprint after the build from about 33 to 17 B/edge, with build time unchanged (#149). Weighted scans keep the previous path, since their weight columns must outlive the build. - Faster and leaner throughout; every kernel's output is unchanged unless noted. Timings on a 4-core machine:
- Index build: known ids are interned in parallel, and scans read 128K-row batches. A 30M-edge Parquet scan and build went from ~2.9s to ~1.65s; with 2M string ids, from ~8.5s to ~3.1s.
- In-memory frames (
from_arrow,from_polars,EdgeFrame(...), ...) no longer keep a concatenated copy of thesrc/dstcolumns next to the table; they share its chunks. On 30M int edges the build went from 2.7s to 1.6s (27 to 22 B/edge over the table); on 20M string edges, from 6.9s and 71 B/edge to 1.9s and 24 B/edge. - String node ids are stored once, in the id column's own buffers, instead of about 140 B of per-id overhead. On 20M edges and 2M ids the build peak fell from 36 to 27 B/edge.
- The transpose that in-direction kernels use is built straight from the out-CSR, without per-row endpoint arrays, so it is no longer the peak: 35 to 30 B/edge on a 30M-edge graph.
triangle_countuses the degree-ordered forward algorithm (6–7× faster on skewed graphs), andclustering_coefficientbenefits too.label_propagation(6×) andlouvain(3–4×) count votes and community weights in dense arrays instead of hash maps. Louvain's aggregation now sums in a fixed order rather than a randomly seeded hash map's.pagerankdivides by out-degree once per node, not per edge (1.8×).- Weight expressions are compiled once and evaluated per batch, and columns that share an expression share one evaluation.
- Smaller items: masks built from packed bits, a bounded CSR-build histogram on many-core machines, and a leaner strongly-connected-components kernel.
- An edge mask whose length does not match the graph's edge count is rejected instead of silently dropping the edges past its end.
0.3.0 — 2026-10-08¶
Added¶
ursa.datasets: bundled graphs that load offline from the wheel (karatewith faction labels,lesmisweighted,florentine,kite), plusfacebook(SNAP ego-Facebook), downloaded on first use and cached under$URSA_DATA_HOME.list_datasets()returns the registry.- Interop constructors
from_edgelist,from_networkx,nodes_from_networkx,from_numpyandfrom_scipy_sparse. networkx, numpy and scipy remain optional and are imported only when the matching constructor is called. group_by(*keys).agg(...), withmean,sum,min,max,countandn_uniqueaggregations, optionally aliased or named throughagg(name=...).join(other, on=, how=): an equi-join of two frames on shared key columns,innerorleft. Distinct from the automatic attribute attach.rename(mapping)andsample(n, seed=)on the shared relational tail.- Filter predicates now lower as a full algebra: string and numeric
comparisons, boolean
&/|/~, arithmetic, and column-to-column comparisons. Previously only a singlecol <op> numbercomparison executed. - Graph operations over a filtered edge frame (subgraph views), and over a
traversal result from
hoporshortest_path. dtype="f32"on the float-valued kernels (pagerank,closeness,betweenness,clustering_coefficient) and onneighbors().agg(), which emits the result column as 32-bit float. The kernel still accumulates in f64 and the emitted value is that result cast down, so it agrees within f32 precision. The integer-valued kernels take nodtype. The default output is unchanged.http(s)://reads for a single hosted Parquet or CSV file.on_null="drop"on edge inputs (EdgeFrame,from_arrow,from_edgelist,scan_edges,read_edges), which filters rows with a nullsrcordstand reports the dropped count as a warning. The default remains"error".- Documentation site at https://cldixon.github.io/ursa/: a quickstart, one guide per task, a reference, a limits page, and a semantics reference that pins each kernel to the NetworkX call it is checked against. Every example on the site runs in the test suite.
Changed¶
- A query naming several graph algorithms now shares work across its columns
instead of recomputing per column. Two columns naming the same kernel with the
same parameters run it once — including the same kernel at two output dtypes,
since
dtype=narrows on emit and so is not part of a computation's identity. Separately,triangle_countandclustering_coefficientin onewith_columnsnow share the sorted-adjacency intersection pass they both rest on, where previously each ran its own; over a subgraph view they also share the masked undirected adjacency, which is rebuilt per use rather than cached. Values are unchanged and bit-identical either way. rayonis now an on-by-default feature of theursa-corecrate. With it disabled the crate compiles forwasm32-unknown-unknown, single-threaded, andrand's default features — and sogetrandom— are dropped, since the stochastic kernels use only seeded ChaCha streams. Serial and parallel outputs are bit-identical. This affects Rust consumers ofursa-coreonly: the Python wheels build with default features and are unchanged.repr()of a collected frame renders the head as a table with the shape, column names and dtypes, capped at 10 rows. It previously showed only a shape and a column list, which left results opaque in a REPL. The preview reads only the rows it displays, so its cost does not scale with row count.to_polars()raises aModuleNotFoundErrornaming theursa-graph[polars]extra when polars is absent, in place of a bare import error.- The README documents installation, and the PyPI project metadata links the documentation site.
version,ModuleTypeandPackageNotFoundErrorare no longer exposed in theursanamespace.__version__and__core_version__are unchanged.
Fixed¶
edges.filter(...).collect()andedges.distinct().collect()return rows; both previously raisedNotImplementedError.
0.2.0 — 2026-08-01¶
Performance improvements and syntax standardization.
0.1.1 — 2026-07-23¶
Fixed a missing Arrow dependency.
0.1.0 — 2026-07-21¶
Initial release.