Layer Set Algebra

The Layer Set Algebra feature provides a first-class, composable system for selecting and manipulating layer sets in multilayer networks. Instead of manually specifying lists of layers, you can use set-theoretic operations to express complex layer selections.

Why Layer Set Algebra?

In multilayer network analysis, you often need to:

  • Select “all layers except the coupling layer”

  • Combine biological layers for aggregate analysis

  • Find the intersection of layers where a node appears

  • Reuse layer group definitions across multiple queries

The Layer Set Algebra makes these operations expressive, composable, and safe.

Quick Start

Basic Operations

from py3plex.dsl import LayerSet, L, Q

# Create layer sets
social = LayerSet("social")
work = LayerSet("work")

# Union: layers in either social OR work
both = social | work

# Intersection: layers in both social AND work
shared = social & work

# Difference: layers in social but NOT in work
social_only = social - work

# Complement: all layers EXCEPT social
not_social = ~social

String Expressions

For convenience, you can write layer expressions as strings:

# Parse from string
layers = LayerSet.parse("* - coupling")

# Or use L[...] syntax (auto-detects expressions)
layers = L["* - coupling"]
layers = L["(ppi | gene) & disease"]
layers = L["social | work | hobby"]

Integration with Queries

Use layer sets in any DSL query:

# Query nodes from all layers except coupling
result = (
    Q.nodes()
     .from_layers(L["* - coupling"])
     .compute("degree")
     .execute(network)
)

# Complex layer selection
result = (
    Q.nodes()
     .from_layers(L["(social | work) & ~coupling"])
     .where(degree__gt=5)
     .execute(network)
)

Set Operations

Union: A | B

The union operation combines layers from multiple sets.

# Select nodes from social OR work layers
layers = L["social | work"]
# Equivalent to:
layers = LayerSet("social") | LayerSet("work")

ASCII Venn Diagram:

┌─────────┐     ┌─────────┐
│ Social  │     │  Work   │
│    ┌────┴─────┴────┐    │
│    │   RESULT      │    │
└────┴────────────────┴────┘
   A | B = {social, work}

Intersection: A & B

The intersection operation selects only layers present in both sets.

# Nodes that appear in both layer A AND layer B
layers = L["layerA & layerB"]

ASCII Venn Diagram:

┌─────────┐     ┌─────────┐
│    A    │     │    B    │
│    ┌────┼─────┼────┐    │
│    │    │ RES │    │    │
└────┴────┼─────┼────┴────┘
          A & B

Difference: A - B

The difference operation removes layers in B from A.

# All layers except coupling
layers = L["* - coupling"]

# Social layers but not bots
layers = L["social - bots"]

ASCII Venn Diagram:

┌─────────┐     ┌─────────┐
│    A    │     │    B    │
│  ┌──────┼─────┤         │
│  │ RES  │     │         │
└──┴──────┼─────┴─────────┘
   A - B = A without B

Complement: ~A

The complement operation returns all layers except those in A.

# Everything except social layer
layers = ~LayerSet("social")

# Equivalent to:
layers = L["* - social"]

ASCII Venn Diagram:

┌─────────────────────────┐
│     All Layers (*)      │
│                         │
│  ┌───────┐              │
│  │   A   │    RESULT    │
│  └───────┘              │
└─────────────────────────┘
   ~A = * - A

Named Layer Groups

Define reusable layer groups for cleaner queries:

from py3plex.dsl import LayerSet, L, Q

# Define a named group
bio_layers = LayerSet("ppi") | LayerSet("gene") | LayerSet("disease")
LayerSet.define_group("bio", bio_layers)

# Or use the convenience method
L.define("core", L["social | work | email"])

# Use the group in queries
result = Q.nodes().from_layers(LayerSet("bio")).execute(network)

# Groups can be combined with other operations
result = Q.nodes().from_layers(L["bio & core"]).execute(network)

# List all defined groups
groups = L.list_groups()
print(groups)  # {'bio': LayerSet(...), 'core': LayerSet(...)}

Operator Precedence

Layer expressions follow standard set theory precedence:

  1. Complement (~) - highest precedence

  2. Intersection (&)

  3. Difference (-)

  4. Union (|) - lowest precedence

Use parentheses for clarity or to override precedence:

# Without parentheses: parsed as A & (B | C)
expr1 = L["A & B | C"]

# With parentheses: explicit grouping
expr2 = L["(A & B) | C"]
expr3 = L["A & (B | C)"]

Real-World Examples

Example 1: Biological Networks

Analyze protein interactions while excluding coupling layers:

from py3plex.dsl import Q, L, LayerSet

# Define biological layer groups
L.define("bio", L["ppi | gene | disease"])

# Find high-degree nodes in biological layers only
result = (
    Q.nodes()
     .from_layers(LayerSet("bio") - LayerSet("coupling"))
     .where(degree__gt=10)
     .compute("betweenness_centrality")
     .order_by("betweenness_centrality", desc=True)
     .limit(20)
     .execute(network)
)

print(result.to_pandas())

Example 2: Social Networks

Compare centrality across different social contexts:

# Define layer groups for different social contexts
L.define("online", L["facebook | twitter | linkedin"])
L.define("offline", L["work | school | hobby"])

# Compute centrality in online layers
online_centrality = (
    Q.nodes()
     .from_layers(LayerSet("online"))
     .compute("pagerank")
     .execute(network)
).to_pandas()

# Compute centrality in offline layers
offline_centrality = (
    Q.nodes()
     .from_layers(LayerSet("offline"))
     .compute("pagerank")
     .execute(network)
).to_pandas()

# Find nodes with high centrality in both contexts
# (merge and filter the DataFrames)

Example 3: Transportation Networks

Analyze robustness by excluding specific infrastructure:

# Simulate failure scenarios
scenarios = [
    ("full_network", L["*"]),
    ("no_air", L["* - air_travel"]),
    ("no_rail", L["* - rail"]),
    ("road_only", L["road"]),
    ("multi_modal", L["road | rail"]),
]

results = {}
for name, layers in scenarios:
    result = (
        Q.nodes()
         .from_layers(layers)
         .compute("betweenness_centrality")
         .execute(network)
    ).to_pandas()

    results[name] = {
        "avg_betweenness": result["betweenness_centrality"].mean(),
        "max_betweenness": result["betweenness_centrality"].max(),
        "node_count": len(result),
    }

import pandas as pd
df_results = pd.DataFrame(results).T
print(df_results)

Introspection and Debugging

Layer sets provide introspection tools to understand what they represent:

Resolution

See which layers a LayerSet resolves to:

layers = L["* - coupling"]

# Resolve against a network
resolved = layers.resolve(network)
print(resolved)  # {'social', 'work', 'hobby'}

Explanation

Get a human-readable explanation of the layer expression:

layers = L["(ppi | gene) & disease"]
print(layers.explain())

# Output:
# LayerSet:
#   intersection(
#     union(
#       layer("ppi"),
#       layer("gene")
#     ),
#     layer("disease")
#   )

With Network Resolution

Combine explanation with network resolution:

layers = L["* - coupling"]
print(layers.explain(network))

# Output:
# LayerSet:
#   difference(
#     all_layers("*"),
#     layer("coupling")
#   )
# → resolved to: {'hobby', 'social', 'work'}

Comparison: Old vs New Syntax

Old Style (Still Supported):

# Manual layer list
result = Q.nodes().from_layers(L["social"] + L["work"]).execute(network)

# Multiple separate queries
layers = ["social", "work", "hobby"]
results = []
for layer in layers:
    if layer != "coupling":
        result = Q.nodes().from_layers(L[layer]).execute(network)
        results.append(result)

New Style (Recommended):

# Expressive layer algebra
result = Q.nodes().from_layers(L["social | work"]).execute(network)

# Single query with filtering
result = (
    Q.nodes()
     .from_layers(L["* - coupling"])
     .execute(network)
)

Why Layer Set Algebra Matters

For Multilayer Networks

Multilayer networks have a unique challenge: layer selection is a first-class operation, not an afterthought. Traditional graph analysis tools don’t provide built-in support for layer algebra, forcing users to write imperative code with loops and conditionals.

Layer Set Algebra brings the following benefits:

  1. Expressiveness: Complex layer selections become one-liners

  2. Composability: Combine layer expressions with set operations

  3. Reusability: Define named groups once, use everywhere

  4. Safety: Late evaluation with validation at execution time

  5. Maintainability: Clear, declarative code is easier to understand

Algebraic Properties

Layer Set Algebra follows standard set theory laws:

  • Idempotence: A | A = A, A & A = A

  • Commutativity: A | B = B | A, A & B = B & A

  • Associativity: (A | B) | C = A | (B | C)

  • Distributivity: A & (B | C) = (A & B) | (A & C)

  • De Morgan’s Laws: ~(A | B) = ~A & ~B, ~(A & B) = ~A | ~B

  • Complement: A | ~A = *, A & ~A = ∅

These properties enable query optimization and reasoning about layer selections.

API Reference

LayerSet Class

class LayerSet:
    """Represents an unevaluated layer expression."""

    def __init__(self, name_or_expr: Union[str, LayerExpr])
        """Create from layer name or expression AST."""

    def __or__(self, other: LayerSet) -> LayerSet:
        """Union: self | other."""

    def __and__(self, other: LayerSet) -> LayerSet:
        """Intersection: self & other."""

    def __sub__(self, other: LayerSet) -> LayerSet:
        """Difference: self - other."""

    def __invert__(self) -> LayerSet:
        """Complement: ~self."""

    def resolve(self, network, *, strict=False, warn_empty=True) -> Set[str]:
        """Resolve expression to actual layer names."""

    def explain(self, network=None) -> str:
        """Generate human-readable explanation."""

    @staticmethod
    def parse(expr_str: str) -> LayerSet:
        """Parse layer expression from string."""

    @staticmethod
    def define_group(name: str, layer_set: LayerSet) -> None:
        """Define a named layer group."""

    @staticmethod
    def list_groups() -> Dict[str, LayerSet]:
        """List all defined groups."""

    @staticmethod
    def clear_groups() -> None:
        """Clear all defined groups."""

L Proxy

The L proxy provides convenient syntax for creating layer expressions:

# Simple layer name (backward compatible)
L["social"]  # Returns LayerExprBuilder

# Expression string (auto-detected)
L["* - coupling"]  # Returns LayerSet
L["social | work"]  # Returns LayerSet

# Define named group
L.define("bio", LayerSet("ppi") | LayerSet("gene"))

# List groups
groups = L.list_groups()

# Clear groups
L.clear_groups()

Troubleshooting

Common Issues

1. Empty Layer Set Warning

layers = L["nonexistent"]
result = layers.resolve(network)
# UserWarning: Layer expression resolved to empty set

Solution: Check layer names, use warn_empty=False to suppress, or use strict=True to raise an error instead.

2. Unknown Layer Error

layers = L["typo_layer"]
result = layers.resolve(network, strict=True)
# UnknownLayerError: Layer 'typo_layer' not found

Solution: Verify layer names with network.layers or network.get_layers().

3. Syntax Error in Expression

layers = L["social &"]  # Missing right operand
# DslSyntaxError: Unexpected end of expression

Solution: Check expression syntax, ensure all operators have operands.

Best Practices

  1. Use Named Groups for complex layer sets you’ll reuse

  2. Prefer String Expressions for readability: L["* - coupling"]

  3. Use Complement instead of listing all other layers: ~LayerSet("exclude")

  4. Document Layer Groups with comments explaining their purpose

  5. Test Layer Resolution with .explain(network) before running expensive queries

See Also

  • query_with_dsl - DSL query basics

  • query_zoo - Query examples and patterns

  • /api/dsl - Complete DSL API reference

Note

Layer Set Algebra is available in py3plex DSL v2.1+. It’s fully backward compatible with existing layer expressions.