from FunctorFlow import (
Diagram,
compile_to_callable,
build_macro,
MACRO_LIBRARY,
)
import jsonMacros and build_macro (Python)
Introduction
In Julia, FunctorFlow provides the @diagram macro — a concise, declarative DSL for building categorical diagrams. Python does not have a macro system, but FunctorFlow’s Python API provides an equivalent capability through two mechanisms:
build_macro()— a factory function that instantiates pre-built architectural patterns from theMACRO_LIBRARY.- Direct builder API —
D.object(),D.morphism(),D.left_kan(), etc. — the imperative interface that the Julia@diagrammacro desugars into.
This vignette demonstrates both approaches and compares them to the Julia DSL.
Setup
The MACRO_LIBRARY
The MACRO_LIBRARY is a dictionary mapping pattern names to factory functions. Each factory returns a fully constructed Diagram.
print("Available macros:")
for name in sorted(MACRO_LIBRARY.keys()):
print(f" {name}")Available macros:
basket_rocket_pipeline
basket_workflow
completion
db_square
democritus_gluing
gt_neighborhood
ket
rocket_repair
structured_lm_duality
Pre-Built Patterns with build_macro()
KET Block (Left Kan Aggregation)
The KET (Kan Extension Transformer) block is the canonical aggregation pattern — a left Kan extension over an incidence relation.
D_ket = build_macro("ket")
print(D_ket.summary())Diagram(KETBlock)
Objects: Values, Incidence, ContextualizedValues
Operations: aggregate
Losses: <none>
Ports: input, relation, output
# The KET block is ready to compile and run
compiled_ket = compile_to_callable(D_ket)
result_ket = compiled_ket.run({
"Values": {"a": 10, "b": 20, "c": 30},
"Incidence": {"x": ["a", "b"], "y": ["b", "c"]}
})
print("KET aggregation (sum):", result_ket.values["aggregate"])KET aggregation (sum): {'x': 30, 'y': 50}
DB Square (Diagrammatic Backpropagation)
The DB Square measures non-commutativity between two composed paths — the foundation of obstruction-based learning.
D_db = build_macro("db_square")
print(D_db.summary())Diagram(DBSquare)
Objects: State
Operations: f, g, p1, p2
Losses: obstruction
Ports: input, left_path, right_path, loss
# Bind implementations to the morphisms
D_db.bind_morphism("f", lambda x: x + 1)
D_db.bind_morphism("g", lambda x: x * 2)
compiled_db = compile_to_callable(D_db)
result_db = compiled_db.run({"State": 3.0})
print(f"f∘g(3) = {result_db.values['p1']}")
print(f"g∘f(3) = {result_db.values['p2']}")
print(f"Obstruction loss: {result_db.losses['obstruction']}")f∘g(3) = 8.0
g∘f(3) = 7.0
Obstruction loss: 1.0
GT Neighborhood Block
The GT (Graph Transformer) Neighborhood block lifts token states into messages and aggregates them via a left Kan extension over a simplicial relation.
D_gt = build_macro("gt_neighborhood")
print(D_gt.summary())Diagram(GTNeighborhoodBlock)
Objects: Tokens, Messages, NeighborhoodIncidence, UpdatedTokens
Operations: lift_messages, kan_aggregate
Losses: <none>
Ports: input, relation, messages, output
# The GT block has a lift morphism and a Kan aggregation
ir = D_gt.to_ir().as_dict()
print("Operations:")
for op in ir["operations"]:
print(f" {op['name']} ({op['kind']})")
print("\nPorts:")
for port in ir["ports"]:
print(f" {port['name']} -> {port['ref']} ({port['direction']})")Operations:
lift_messages (morphism)
kan_aggregate (kanextension)
Ports:
input -> Tokens (input)
relation -> NeighborhoodIncidence (input)
messages -> Messages (internal)
output -> kan_aggregate (output)
Customizing Macros with Keyword Arguments
Each build_macro() call accepts keyword arguments that override the default configuration. This lets you customize object names, reducers, and other parameters.
# KET block with mean reducer instead of sum
D_mean = build_macro("ket", reducer="mean")
compiled_mean = compile_to_callable(D_mean)
result_mean = compiled_mean.run({
"Values": {"a": 10.0, "b": 20.0, "c": 30.0},
"Incidence": {"x": ["a", "b"], "y": ["b", "c"]}
})
print("KET aggregation (mean):", result_mean.values["aggregate"])KET aggregation (mean): {'x': 15.0, 'y': 25.0}
# DB Square with custom morphism names
D_custom = build_macro("db_square",
first_morphism="encode",
second_morphism="decode")
print(D_custom.summary())Diagram(DBSquare)
Objects: State
Operations: encode, decode, p1, p2
Losses: obstruction
Ports: input, left_path, right_path, loss
Manual Diagram Construction
For full control, you can construct diagrams imperatively. This is the Python equivalent of what the Julia @diagram macro desugars into.
Basic Diagram (Julia @diagram equivalent)
In Julia, you would write:
D = @diagram SimpleTransform begin
@object X kind=:input description="Input vector"
@object Y kind=:output description="Output vector"
@morphism transform X Y
endIn Python, the equivalent is:
D = Diagram("SimpleTransform")
D.object("X", kind="input", description="Input vector")
D.object("Y", kind="output", description="Output vector")
D.morphism("transform", "X", "Y")
print(D.summary())Diagram(SimpleTransform)
Objects: X, Y
Operations: transform
Losses: <none>
Ports: <none>
Composition
Composition uses self-morphisms for natural chaining:
D3 = Diagram("Pipeline")
D3.object("S", kind="state")
D3.morphism("step_a", "S", "S")
D3.morphism("step_b", "S", "S")
D3.compose("step_a", "step_b", name="full_pipeline")
d3 = D3.to_ir().as_dict()
print("Operations:", [op["name"] for op in d3["operations"]])Operations: ['step_a', 'step_b', 'full_pipeline']
We can bind implementations and run:
D3.bind_morphism("step_a", lambda x: x + 10)
D3.bind_morphism("step_b", lambda x: x * 2)
compiled = compile_to_callable(D3)
result = compiled.run({"S": 5})
print("step_a(5) =", result.values["step_a"])
print("step_b(5) =", result.values["step_b"])
print("Pipeline (step_b ∘ step_a)(5) =", result.values["full_pipeline"])step_a(5) = 15
step_b(5) = 10
Pipeline (step_b ∘ step_a)(5) = 30
Kan Extensions
D4 = Diagram("Aggregator")
D4.object("Messages", kind="messages")
D4.object("Neighbors", kind="relation")
D4.object("Pooled", kind="output")
D4.left_kan(source="Messages", along="Neighbors",
target="Pooled", name="pool", reducer="mean")
compiled4 = compile_to_callable(D4)
result4 = compiled4.run({
"Messages": {"a": 10.0, "b": 20.0, "c": 30.0},
"Neighbors": {"x": ["a", "b"], "y": ["b", "c"]}
})
print("Pooled (mean):", result4.values["pool"])Pooled (mean): {'x': 15.0, 'y': 25.0}
Right Kan extensions use D.right_kan() and default to the "first_non_null" reducer:
D5 = Diagram("Completer")
D5.object("Partial", kind="partial")
D5.object("Compat", kind="relation")
D5.right_kan(source="Partial", along="Compat", name="repair",
reducer="first_non_null")
compiled5 = compile_to_callable(D5)
result5 = compiled5.run({
"Partial": {"a": None, "b": 42, "c": None},
"Compat": {"a": ["b", "c"], "c": ["b"]}
})
print("Repaired:", result5.values["repair"])Repaired: {'a': 42, 'c': 42}
Obstruction Loss
D6 = Diagram("DBSquareDemo")
D6.object("S", kind="state")
D6.morphism("f", "S", "S")
D6.morphism("g", "S", "S")
D6.compose("f", "g", name="fg")
D6.compose("g", "f", name="gf")
D6.obstruction_loss(paths=[("fg", "gf")], name="consistency",
comparator="l2", weight=1.0)
D6.bind_morphism("f", lambda x: x + 1)
D6.bind_morphism("g", lambda x: x * 2)
compiled6 = compile_to_callable(D6)
result6 = compiled6.run({"S": 3.0})
print(f"f∘g(3) = {result6.values['fg']}")
print(f"g∘f(3) = {result6.values['gf']}")
print(f"Obstruction loss: {result6.losses['consistency']}")f∘g(3) = 8.0
g∘f(3) = 7.0
Obstruction loss: 1.0
The loss measures \(\|f(g(x)) - g(f(x))\|_2\) — the degree to which \(f\) and \(g\) fail to commute.
Ports
D7 = Diagram("PortedModel")
D7.object("Tokens", kind="messages")
D7.object("Neighbors", kind="relation")
D7.object("Output", kind="contextualized_messages")
D7.left_kan(source="Tokens", along="Neighbors",
target="Output", name="aggregate", reducer="sum")
D7.expose_port("input", "Tokens", direction="input", port_type="messages")
D7.expose_port("relation", "Neighbors", direction="input", port_type="relation")
D7.expose_port("output", "aggregate", direction="output",
port_type="contextualized_messages")
d7 = D7.to_ir().as_dict()
print("Ports:", [p["name"] for p in d7["ports"]])Ports: ['input', 'relation', 'output']
Comparing: build_macro vs Manual Construction
Here is the same KET diagram built with build_macro() and with the manual builder API.
build_macro:
D_macro = build_macro("ket")
print(f"Macro diagram: {D_macro.name} — "
f"{len(D_macro.objects)} objects, "
f"{len(D_macro.operations)} operations, "
f"{len(D_macro.ports)} ports")Macro diagram: KETBlock — 3 objects, 1 operations, 3 ports
Manual builder:
D_manual = Diagram("KETManual")
D_manual.object("Values", kind="messages")
D_manual.object("Incidence", kind="relation")
D_manual.object("ContextualizedValues", kind="contextualized_messages")
D_manual.left_kan(source="Values", along="Incidence",
target="ContextualizedValues", name="aggregate",
reducer="sum")
D_manual.expose_port("input", "Values", direction="input", port_type="messages")
D_manual.expose_port("relation", "Incidence", direction="input", port_type="relation")
D_manual.expose_port("output", "aggregate", direction="output",
port_type="contextualized_messages")
print(f"Manual diagram: {D_manual.name} — "
f"{len(D_manual.objects)} objects, "
f"{len(D_manual.operations)} operations, "
f"{len(D_manual.ports)} ports")Manual diagram: KETManual — 3 objects, 1 operations, 3 ports
Both produce structurally equivalent diagrams.
Julia vs Python: A Comparison
| Feature | Julia | Python |
|---|---|---|
| Declarative DSL | @diagram macro |
Not available |
| Pre-built patterns | build_block(:ket) |
build_macro("ket") |
| Manual construction | add_object!(D, :X; kind=:input) |
D.object("X", kind="input") |
| Morphism binding | bind_morphism!(D, :f, fn) |
D.bind_morphism("f", fn) |
| Kan extensions | add_left_kan!(D, :k; ...) |
D.left_kan(source=..., name="k") |
| Obstruction loss | add_obstruction_loss!(D, :loss; ...) |
D.obstruction_loss(paths=..., name="loss") |
| Names | Symbols (:X) |
Strings ("X") |
In Julia, the @diagram macro provides a concise DSL that reduces boilerplate. In Python, use build_macro() for pre-built patterns or construct diagrams manually with the builder API. Both approaches produce the same underlying Diagram objects and can be compiled and executed identically.