Aggregation#
Aggregation reduces a partitioned network by merging nodes within each cluster into single representative nodes. NPAP uses a three-tier aggregation system that separates topology creation, physical aggregation, and statistical property aggregation.
Overview#
The Three-Tier System#
flowchart LR
A[Partitioned Graph] --> B[1. Topology Creation]
B --> C[2. Physical Aggregation]
C --> D[3. Statistical Aggregation]
D --> E[Aggregated Graph]
style A fill:#2993B5,stroke:#1d6f8a,color:#fff
style B fill:#2993B5,stroke:#1d6f8a,color:#fff
style C fill:#0fad6b,stroke:#076b3f,stroke-dasharray: 5 5,color:#fff
style D fill:#2993B5,stroke:#1d6f8a,color:#fff
style E fill:#2993B5,stroke:#1d6f8a,color:#fff
Topology Creation: Creates the structure of the aggregated graph (nodes and edges)
Physical Aggregation: Applies electrical laws to preserve physical behavior (optional)
Statistical Aggregation: Aggregates node and edge properties
Quick Start#
import npap
from npap import AggregationMode
manager = npap.PartitionAggregatorManager()
manager.load_data("networkx_direct", graph=G)
partition = manager.partition("geographical_kmeans", n_clusters=10)
# Aggregate using a predefined mode
aggregated = manager.aggregate(mode=AggregationMode.GEOGRAPHICAL)
Aggregation Modes#
NPAP provides predefined aggregation modes for common use cases.
Available Modes#
Mode |
Description |
Use Case |
|---|---|---|
|
Sum all numeric properties |
Basic reduction |
|
Average coordinates, sum loads |
Spatial analysis |
|
Kron reduction for DC networks |
DC network analysis |
|
User-defined profile |
Advanced use |
SIMPLE Mode#
Sums all numeric properties across clusters:
from npap import AggregationMode
aggregated = manager.aggregate(mode=AggregationMode.SIMPLE)
Configuration:
Topology:
simpleNode properties:
sumEdge properties:
sum
GEOGRAPHICAL Mode#
Designed for networks with geographic coordinates:
aggregated = manager.aggregate(mode=AggregationMode.GEOGRAPHICAL)
Configuration:
Topology:
simpleNode
lat,lon:average(geographic center)Node
base_voltage:averageEdge
p_max:sumEdge
x:averageDefault:
average
CUSTOM Mode#
For full control, create an AggregationProfile:
from npap import AggregationProfile
profile = AggregationProfile(
topology_strategy="simple",
physical_strategy=None,
node_properties={
"lat": "average",
"lon": "average",
"load": "sum",
"generation": "sum"
},
edge_properties={
"x": "equivalent_reactance",
"r": "equivalent_reactance",
"p_max": "sum"
},
default_node_strategy="sum",
default_edge_strategy="average"
)
aggregated = manager.aggregate(profile=profile)
Aggregation Profile#
The AggregationProfile controls all aspects of aggregation.
Parameters#
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
str |
|
How to create graph structure |
|
str | None |
|
Physical aggregation method |
|
list[str] |
|
Properties for physical aggregation |
|
dict |
|
Extra parameters for physical strategy |
|
dict[str, str] |
|
Property → aggregation method mapping |
|
dict[str, str] |
|
Property → aggregation method mapping |
|
str |
|
Fallback for unmapped node properties |
|
str |
|
Fallback for unmapped edge properties |
|
bool |
|
Warn when using default strategies |
|
AggregationMode |
|
Associated mode |
Example Profiles#
Power Flow Analysis:
power_flow_profile = AggregationProfile(
topology_strategy="simple",
node_properties={
"p_load": "sum",
"q_load": "sum",
"p_gen": "sum",
"q_gen": "sum",
"voltage": "average"
},
edge_properties={
"x": "equivalent_reactance",
"r": "equivalent_reactance",
"b": "sum",
"p_max": "sum"
}
)
Geographic Visualization:
geo_profile = AggregationProfile(
topology_strategy="simple",
node_properties={
"lat": "average",
"lon": "average",
"name": "first"
},
edge_properties={
"length": "sum"
},
default_node_strategy="first",
default_edge_strategy="first"
)
Topology Strategies#
Topology strategies determine how the aggregated graph structure is created.
Simple Topology#
Creates edges only where connections existed in the original graph:
profile = AggregationProfile(topology_strategy="simple")
One node per cluster
Edge between clusters if any original edge connected them
Preserves network structure
flowchart LR
subgraph Original
A1[A1] --> B1[B1]
A2[A2] --> B1
A1 --> A2
end
subgraph Aggregated
A[Cluster A] --> B[Cluster B]
end
Original --> Aggregated
style A1 fill:#64748b,stroke:#475569,color:#fff
style A2 fill:#64748b,stroke:#475569,color:#fff
style B1 fill:#64748b,stroke:#475569,color:#fff
style A fill:#2993B5,stroke:#1d6f8a,color:#fff
style B fill:#0fad6b,stroke:#076b3f,color:#fff
Electrical Topology#
For use with physical aggregation strategies:
profile = AggregationProfile(
topology_strategy="electrical",
connectivity="existing" # or "full"
)
Connectivity modes:
existing: Same as simple topologyfull: Creates fully connected graph
Property Aggregation Strategies#
Node Property Strategies#
Strategy |
Formula |
Use Case |
|---|---|---|
|
\(\sum_{i \in C} x_i\) |
Loads, generation, counts |
|
\(\frac{1}{\lvert C\rvert}\sum_{i \in C} x_i\) |
Coordinates, voltage |
|
\(x_1\) |
Names, IDs, categories |
Examples:
node_properties={
"load": "sum", # Total load in cluster
"lat": "average", # Geographic center
"name": "first", # Keep first name
"voltage": "average" # Average voltage level
}
Edge Property Strategies#
Strategy |
Formula |
Use Case |
|---|---|---|
|
\(\sum_{e \in E} x_e\) |
Capacity, flow limits |
|
\(\frac{1}{\lvert E\rvert}\sum_{e \in E} x_e\) |
General properties |
|
\(x_1\) |
Type, category |
|
\(\frac{1}{\sum_{e \in E} \frac{1}{x_e}}\) |
Parallel impedances |
Examples:
edge_properties={
"p_max": "sum", # Total capacity
"x": "equivalent_reactance", # Parallel reactance
"length": "average", # Average length
"type": "first" # Keep first type
}
Equivalent Reactance#
For parallel transmission lines, reactances combine as:
This is the electrical equivalent of parallel resistors/inductors.
edge_properties={
"x": "equivalent_reactance",
"r": "equivalent_reactance"
}
Physical Aggregation#
Physical aggregation strategies preserve electrical laws during network reduction.
Kron Reduction (Planned)#
Note
Kron reduction is planned for a future release.
Handling Defaults#
When a property isn’t explicitly mapped, NPAP uses the default strategy:
profile = AggregationProfile(
node_properties={"load": "sum"}, # Only load is mapped
default_node_strategy="average", # Everything else uses average
warn_on_defaults=True # Warn when using defaults
)
Warnings#
With warn_on_defaults=True, you’ll see warnings like:
UserWarning: Using default strategy 'average' for node property 'voltage'
This helps identify properties you may want to explicitly configure.
Aggregating Parallel Edges#
Before partitioning, you need to aggregate parallel edges in a MultiDiGraph:
# Load data (may return MultiDiGraph)
graph = manager.load_data("csv_files", node_file="...", edge_file="...")
# Aggregate parallel edges
if isinstance(graph, nx.MultiDiGraph):
manager.aggregate_parallel_edges(
edge_properties={
"x": "equivalent_reactance",
"r": "equivalent_reactance",
"p_max": "sum",
"length": "average"
},
default_strategy="average",
warn_on_defaults=False
)
This converts the MultiDiGraph to a DiGraph with single edges.
Complete Workflow Example#
import npap
from npap import AggregationProfile, AggregationMode
import networkx as nx
# Create manager
manager = npap.PartitionAggregatorManager()
# Load data
graph = manager.load_data("csv_files",
node_file="nodes.csv",
edge_file="edges.csv")
# Handle parallel edges if present
if isinstance(graph, nx.MultiDiGraph):
manager.aggregate_parallel_edges(
edge_properties={"x": "equivalent_reactance", "p_max": "sum"}
)
# Partition
partition = manager.partition("geographical_kmeans", n_clusters=20)
# Option 1: Use predefined mode
aggregated = manager.aggregate(mode=AggregationMode.GEOGRAPHICAL)
# Option 2: Use custom profile
custom_profile = AggregationProfile(
topology_strategy="simple",
node_properties={
"lat": "average",
"lon": "average",
"load": "sum",
"generation": "sum"
},
edge_properties={
"x": "equivalent_reactance",
"p_max": "sum"
},
default_node_strategy="sum",
default_edge_strategy="sum"
)
aggregated = manager.aggregate(profile=custom_profile)
# Inspect result
print(f"Original: {graph.number_of_nodes()} nodes, {graph.number_of_edges()} edges")
print(f"Aggregated: {aggregated.number_of_nodes()} nodes, {aggregated.number_of_edges()} edges")
Accessing Mode Profiles#
You can retrieve the profile for any predefined mode:
from npap import get_mode_profile, AggregationMode
# Get the GEOGRAPHICAL mode profile
geo_profile = get_mode_profile(AggregationMode.GEOGRAPHICAL)
print(geo_profile.topology_strategy)
# 'simple'
print(geo_profile.node_properties)
# {'lat': 'average', 'lon': 'average', 'base_voltage': 'average'}
This is useful for:
Inspecting what a mode does
Using a mode as a starting point for customization
# Start from GEOGRAPHICAL and customize
profile = get_mode_profile(AggregationMode.GEOGRAPHICAL)
profile.node_properties["custom_attr"] = "sum"
profile.edge_properties["custom_edge"] = "average"
aggregated = manager.aggregate(profile=profile)
Best Practices#
1. Choose Appropriate Strategies#
Property Type |
Recommended Strategy |
|---|---|
Extensive (load, generation) |
|
Intensive (voltage, temperature) |
|
Impedance (reactance, resistance) |
|
Categorical (name, type) |
|
Capacity (p_max, rating) |
|
Length, distance |
|
2. Validate Results#
Check that aggregation preserves important quantities:
# Total load should be preserved
original_load = sum(graph.nodes[n].get("load", 0) for n in graph.nodes())
aggregated_load = sum(aggregated.nodes[n].get("load", 0) for n in aggregated.nodes())
assert abs(original_load - aggregated_load) < 1e-6, "Load not conserved!"
3. Handle Missing Properties#
Properties may not exist on all nodes/edges:
# Check for missing properties before aggregation
missing = [n for n in graph.nodes() if "load" not in graph.nodes[n]]
if missing:
print(f"Warning: {len(missing)} nodes missing 'load' attribute")
4. Document Your Profile#
When using custom profiles, document your choices:
profile = AggregationProfile(
topology_strategy="simple",
node_properties={
"lat": "average", # Geographic center of cluster
"lon": "average", # Geographic center of cluster
"p_load": "sum", # Total active load (MW)
"q_load": "sum", # Total reactive load (MVAr)
},
edge_properties={
"x": "equivalent_reactance", # Parallel combination
"p_max": "sum", # Total transfer capacity
}
)