ACSets: Algebraic Databases Skill
"The category of simple graphs does not even have a terminal object!" <<<<<<< HEAD — AlgebraicJulia Blog, with characteristic ironic detachment
What Are ACSets?
ACSets ("attributed C-sets") are a family of data structures generalizing both graphs and data frames. They are an efficient in-memory implementation of a category-theoretic formalism for relational databases.
C-set = Functor X: C → Set where C is a small category (schema)
┌─────────────────────────────────────────────────────────────┐
│ Schema (Small Category C) │
│ ┌─────┐ src ┌─────┐ │
│ │ E │───────▶│ V │ │
│ │ │ tgt │ │ │
│ └──┬──┘───────▶└─────┘ │
│ │ │
│ │ A C-set X assigns: │
│ │ X(V) = set of vertices │
│ │ X(E) = set of edges │
│ │ X(src): X(E) → X(V) │
│ │ X(tgt): X(E) → X(V) │
└─────────────────────────────────────────────────────────────┘
Core Concepts
1. Schema Definition
using Catlab.CategoricalAlgebra
@present SchGraph(FreeSchema) begin
V::Ob
E::Ob
src::Hom(E,V)
tgt::Hom(E,V)
end
@acset_type Graph(SchGraph, index=[:src,:tgt])
2. Symmetric Graphs (Undirected)
@present SchSymmetricGraph <: SchGraph begin
inv::Hom(E,E)
compose(inv,src) == tgt
compose(inv,tgt) == src
compose(inv,inv) == id(E)
end
@acset_type SymmetricGraph(SchSymmetricGraph, index=[:src])
3. Attributed ACSets (with Data)
=======
> — AlgebraicJulia Blog
## What Are ACSets?
**C-set** = Functor `X: C → Set` where C is a small category (schema)
Schema C C-set X: C → Set ┌─────┐ src X(V) = {v1,v2,v3} │ E │──────▶ X(E) = {e1,e2} │ │ tgt X(src)(e1) = v1 └─────┘──────▶ X(tgt)(e1) = v2
---
## Core Schemas
```julia
@present SchGraph(FreeSchema) begin
V::Ob; E::Ob
src::Hom(E,V); tgt::Hom(E,V)
end
@acset_type Graph(SchGraph, index=[:src,:tgt])
# Symmetric (undirected)
@present SchSymmetricGraph <: SchGraph begin
inv::Hom(E,E)
compose(inv,src) == tgt
compose(inv,inv) == id(E)
end
# Attributed
>>>>>>> origin/feature/skill-connectivity-hub-20251226
@present SchWeightedGraph <: SchGraph begin
Weight::AttrType
weight::Attr(E, Weight)
end
<<<<<<< HEAD
@acset_type WeightedGraph(SchWeightedGraph, index=[:src,:tgt]){Float64}
GF(3) Conservation for ACSets
Integrate with Music Topos 3-coloring:
# Map ACSet parts to trits for GF(3) conservation
function acset_to_trits(g::Graph, seed::UInt64)
rng = SplitMix64(seed)
trits = Int[]
for e in parts(g, :E)
h = next_u64!(rng)
hue = (h >> 16 & 0xffff) / 65535.0 * 360
trit = hue < 60 || hue >= 300 ? 1 :
hue < 180 ? 0 : -1
push!(trits, trit)
end
trits
end
# Verify conservation: sum(trits) ≡ 0 (mod 3)
function gf3_conserved(trits)
sum(trits) % 3 == 0
end
Higher-Order Functions on ACSets
From Issue #7, implement functional patterns:
| Function | Description | Example |
|---|---|---|
map |
Transform parts | map(g, :E) do e; ... end |
filter |
Select parts by predicate | `filter(g, :V) { |
fold |
Aggregate over parts | fold(+, g, :E, :weight) |
Open ACSets (Composable Interfaces)
# From Issue #89: Open versions of InterType ACSets
using ACSets.OpenACSetTypes
# Create open ACSet with exposed ports
@open_acset_type OpenGraph(SchGraph, [:V])
# Compose via pushout
g1 = OpenGraph(...) # ports: v1, v2
g2 = OpenGraph(...) # ports: v3, v4
g_composed = compose(g1, g2, [:v2 => :v3])
Blog Post Series
- Graphs and C-sets I: What is a graph?
- Graphs and C-sets II: Half-edges and rotation systems
- Graphs and C-sets III: Reflexive graphs and C-set homomorphisms
- Graphs and C-sets IV: Propositional logic of subgraphs
Citation
@article{patterson2022categorical,
title={Categorical data structures for technical computing},
author={Patterson, Evan and Lynch, Owen and Fairbanks, James},
journal={Compositionality},
volume={4},
number={5},
year={2022},
doi={10.32408/compositionality-4-5}
}
Related Packages
- Catlab.jl: Full categorical algebra (homomorphisms, limits, colimits)
- AlgebraicRewriting.jl: Declarative rewriting for ACSets
- AlgebraicDynamics.jl: Compositional dynamical systems
Xenomodern Integration
The ironic detachment comes from recognizing that:
- Category theory isn't about abstraction for its own sake — it's about finding the right abstractions that compose
- Simple graphs are actually badly behaved — the terminal object problem reveals hidden assumptions
- Functors are data structures — this reframes databases as applied category theory
xenomodernity
│
┌─────────┴─────────┐
│ │
ironic sincere
detachment engagement
│ │
└─────────┬─────────┘
│
C-sets as functors
(both ironic AND sincere)
Commands
just acset-demo # Run ACSet demonstration
just acset-graph # Create and visualize graph
just acset-symmetric # Symmetric graph example
just acset-gf3 # Check GF(3) conservation
======= @acset_type WeightedGraph(SchWeightedGraph){Float64}
---
## ∫G: Category of Elements
Converts ACSet into category (shape for decompositions):
```julia
G = @acset Graph begin V=3; E=2; src=[1,2]; tgt=[2,3] end
∫G = ∫(G) # Objects: (V,1),(V,2),(V,3),(E,1),(E,2)
Wiring Diagrams (AlgebraicDynamics)
@present SchUWD(FreeSchema) begin
Box::Ob; Port::Ob; Junction::Ob; OuterPort::Ob
box::Hom(Port, Box)
junction::Hom(Port, Junction)
outer_junction::Hom(OuterPort, Junction)
end
# oapply = colimit of component diagram
composite = oapply(wiring_diagram, components)
Time-Varying ACSets
@present SchTimeVarying(FreeSchema) begin
Interval::Ob; Snapshot::Ob; State::Ob
timestamp::Hom(Snapshot, Interval)
observable::Hom(Snapshot, State)
Time::AttrType; Value::AttrType
end
# Colimit reconstructs dynamics (DMD = colimit of snapshot diagram)
𝐃 Functor: Lifting Problems
# k-coloring as functor
k_coloring(G) = homomorphisms(G, complete_graph(k))
# Lift to solution decomposition
solution = 𝐃(k_coloring, decomp, CoDecomposition)
(answer, witness) = decide_sheaf_tree_shape(k_coloring, decomp)
Open ACSets
@open_acset_type OpenGraph(SchGraph, [:V])
g_composed = compose(g1, g2, [:v2 => :v3]) # Pushout
GF(3) Triads
bumpus-width (-1) ⊗ acsets (0) ⊗ libkind-directed (+1) = 0 ✓ [FPT]
schema-valid (-1) ⊗ acsets (0) ⊗ oapply-colimit (+1) = 0 ✓ [Composition]
temporal-coal (-1) ⊗ acsets (0) ⊗ koopman-gen (+1) = 0 ✓ [Dynamics]
Specter Navigation (NEW 2025-12-22)
Zero-overhead ACSet navigation via Specter-style paths:
Benchmark Results
| Operation | Hand-Written | Optimized Specter | Ratio |
|---|---|---|---|
| Select field | - | 386.4 ns | ~2x |
| Navigate 4 levels | 735.7 ns | 394.9 ns | 1.9x faster |
ACSet Navigators
# Navigate schema objects and morphisms
acset_field(:E, :src) # Select all source vertices
acset_field(:E, :tgt) # Select all target vertices
acset_where(:E, :src, ==(1)) # Filter edges by predicate
acset_parts(:V) # Navigate to all parts of object
# Compose paths
path = (acset_parts(:E), acset_field(:E, :src))
select(path, graph) # All source vertices of all edges
Correct-by-Construction
ACSet navigation follows 3-MATCH principles:
- Local: Field names checked at compile time
- Global: Path correctness guaranteed
- GF(3): sheaf-cohomology (-1) ⊗ acsets (0) ⊗ gay-mcp (+1) = 0
Edge Growth Rules (NEW 2025-12-22)
ACSet-based edge growth with spectral constraints:
Ramanujan-Preserving Growth
@present SchRamanujanGraph <: SchGraph begin
SpectralData::AttrType
lambda2::Attr(V, SpectralData) # Track λ₂ per growth step
end
function grow_edge_ramanujan!(G::ACSet, u, v)
"""
Add edge preserving Ramanujan property.
Uses Alon-Boppana bound: λ₂ ≥ 2√(d-1).
"""
d = degree(G)
bound = 2 * sqrt(d - 1)
# Tentatively add edge
add_part!(G, :E, src=u, tgt=v)
# Check spectral constraint
λ₂ = second_eigenvalue(adjacency_matrix(G))
if λ₂ > bound + 0.01 # Tolerance
# Rollback: remove edge
rem_part!(G, :E, nparts(G, :E))
return false
end
return true
end
Non-Backtracking Edge Schema
@present SchNonBacktracking(FreeSchema) begin
V::Ob; E::Ob; DE::Ob # DE = directed edges
src::Hom(E,V); tgt::Hom(E,V)
forward::Hom(E, DE); backward::Hom(E, DE)
de_src::Hom(DE, V); de_tgt::Hom(DE, V)
# Non-backtracking constraint: head(e) = tail(f) ∧ e ≠ f⁻¹
nonbacktrack::Hom(DE, DE) # B matrix as morphism
end
# Ihara zeta via ACSet homomorphisms
function prime_cycles(G::ACSet, max_length)
cycles = []
for k in 1:max_length
if moebius(k) != 0 # Only squarefree lengths
push!(cycles, find_cycles(G, k))
end
end
return cycles
end
Centrality via Möbius Inversion
function alternating_centrality(G::ACSet)
"""
Centrality via Möbius-weighted path counts.
c(v) = Σ_{k} μ(k) × paths_k(v) / k
"""
n = nparts(G, :V)
A = adjacency_matrix(G)
c = zeros(n)
for k in 1:diameter(G)
μ_k = moebius(k)
if μ_k != 0
paths_k = diag(A^k)
c .+= μ_k .* paths_k ./ k
end
end
return c ./ sum(abs.(c))
end
Spectral Bundle Triads
ramanujan-expander (-1) ⊗ acsets (0) ⊗ gay-mcp (+1) = 0 ✓ [Graph Coloring]
ramanujan-expander (-1) ⊗ acsets (0) ⊗ moebius-inversion (+1) = 0 ✓ [Edge Growth]
ihara-zeta (-1) ⊗ acsets (0) ⊗ moebius-inversion (+1) = 0 ✓ [Prime Cycles]
StructACSet Internals (DeepWiki 2025-12-22)
Schema and attribute types known at compile time, enabling performance optimizations.
Type Parameters
StructACSet{S, Ts, PT}
# S = TypeLevelSchema{Symbol} - schema at compile time
# Ts = Tuple of Julia types for attributes (e.g., Float64 for Weight)
# PT = PartsType strategy (IntParts or BitSetParts)
Column Storage
struct StructACSet{S, Ts, PT}
parts::NamedTuple # {:V => IntParts, :E => IntParts, ...}
subparts::NamedTuple # {:src => Column, :tgt => Column, :weight => Column, ...}
end
# Column types:
# - Homs: Vector{Int} mapping parts to parts
# - Attrs: Vector{Union{AttrVar,T}} for attribute values
Index Configuration
@acset_type Graph(SchGraph,
index=[:src, :tgt], # Preimage cache: O(1) incident queries
unique_index=[:inv] # Injective cache: even faster
)
# Index types:
# - NoIndex: Linear scan O(n)
# - Index (StoredPreimageCache): O(1) average via hash
# - UniqueIndex (InjectiveCache): O(1) guaranteed, injective morphisms only
Part ID Allocation Strategies
| Strategy | Type | Deletion | Use Case |
|---|---|---|---|
| IntParts | DenseParts | Pop-and-swap (renumbers) | Fast, compact storage |
| BitSetParts | MarkAsDeleted | Preserves IDs, requires gc!() | Stable references |
# IntParts (default): contiguous IDs 1..n
add_parts!(G, :V, 3) # IDs: 1, 2, 3
rem_part!(G, :V, 2) # ID 3 → 2, ID 2 gone
# BitSetParts: sparse IDs with gaps
rem_part!(G, :V, 2) # ID 2 marked deleted, ID 3 unchanged
gc!(G) # Compact: removes gaps
Performance Optimizations
- Compile-time dispatch:
Val{S},Val{Ts}for static method selection - Columnar storage: Cache-friendly, vectorizable access patterns
- Preimage caching:
incident(G, v, :src)is O(1) with index @ct_enable: Compile-time schema validation in_set_subpart!
ACSet Variants
| Variant | Schema | Types | Performance |
|---|---|---|---|
| StructACSet | Type parameter | Type parameter | Optimal (compile-time) |
| DynamicACSet | Field value | Field value | Flexible, runtime cost |
| AnonACSet | Type parameter | Type parameter | Works with any schema |
DeepWiki Integration (Verified 2025-12-22)
| DeepWiki Query | ACSets Skill | Match |
|---|---|---|
| "ACSet = functor from schema to Set" | C-set = Functor X: C → Set |
✓ |
| "StructACSet uses type parameters" | Column storage + PT | ✓ |
| "Index optimizes incident queries" | O(1) via StoredPreimageCache | ✓ |
| "IntParts uses pop-and-swap" | Part ID allocation strategies | ✓ |
Cross-Skill Synergy
# DeepWiki query → ACSet implementation → Spectral analysis
mcp__deepwiki__ask_question("AlgebraicJulia/ACSets.jl",
"How does incident query work with indices?")
# Answer: StoredPreimageCache maintains Dict{Int, Vector{Int}}
# Apply: incident(G, v, :src) retrieves precomputed edge list
# Combine with Ramanujan edge growth
grow_edge_ramanujan!(G, u, v) # Uses add_part!, incident, rem_part!
Related
- Catlab.jl: Homomorphisms, limits, colimits
- StructuredDecompositions.jl: ∫G, 𝐃 functor, FPT
- AlgebraicDynamics.jl: Wiring diagrams, oapply
- SpecterOptimized.jl: Zero-overhead navigation
- ramanujan-expander: Alon-Boppana spectral bounds
- ihara-zeta: Non-backtracking walks and zeta functions
- moebius-inversion: Alternating sums on posets
- deepwiki-mcp: Repository documentation (trit 0, substitutes in triads)
origin/feature/skill-connectivity-hub-20251226