Skip to content

Add graph theory partitioners and conservation mode#19

Open
omari91 wants to merge 24 commits intoIEE-TUGraz:mainfrom
omari91:main
Open

Add graph theory partitioners and conservation mode#19
omari91 wants to merge 24 commits intoIEE-TUGraz:mainfrom
omari91:main

Conversation

@omari91
Copy link
Copy Markdown

@omari91 omari91 commented Feb 19, 2026

Summary

Brief description of what this PR does.

Changes

  • Change 1
  • Change 2

Testing

Describe how you tested your changes.

  • All tests pass: pytest
  • Code is formatted: ruff format .
  • No linting errors: ruff check .
  • Documentation updated (if applicable)

Related Issues

Closes #

@omari91
Copy link
Copy Markdown
Author

omari91 commented Feb 19, 2026

Summary:
Adds graph-theory partitioners plus a transformer-conservation aggregation mode with matching docs/presets so the workflow, aggregation physics, and visualization pipeline stay aligned.

  • Changes:
    • Introduced SpectralPartitioning and CommunityPartitioning, registered them with PartitioningManager, and documented them under “Available Strategies”.
    • Added TransformerConservationStrategy, exposed via AggregationMode.CONSERVATION, and documented the mode plus a new “Workflows” guide that shows how to combine community-aware partitioning with conservation-mode aggregation and preset-driven visualization.
    • Extended npap.visualization with PlotPreset and a preset argument for plot_network, then documented the presets.
  • Testing: python -m pytest (243 tests); python -m ruff check . (0 lint errors).

@IEE-TUGraz IEE-TUGraz deleted a comment from codecov bot Feb 20, 2026
Copy link
Copy Markdown
Member

@MarcoAnarmo MarcoAnarmo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @omari91! First of all, thank you so much for taking the time to put this together — graph-theory partitioners and the transformer conservation mode are awesome additions and really align well with NPAP 😃

I've gone through all your changes and left some inline comments. Here's a quick summary of the broader things:

  • Documentation: The new classes (SpectralPartitioning, CommunityPartitioning, TransformerConservationStrategy, PlotPreset) would need entries in the API reference files under docs/api/ so they get picked up by the auto-generated documentation. That way users can discover them easily alongside the existing strategies. Also make sure the docstrings follow the NumPy style we use throughout. You can check geographical.py or basic_strategies.py for reference!

  • Tests: You've got good coverage on the happy paths already, which is great. It would be nice to add a couple more cases to make things really solid, like an end-to-end test for AggregationMode.CONSERVATION, and what happens when there are no transformers in the graph.

  • Commit practices: Just a small tip for future PRs: it really helps if you work on a feature branch (something like feature/graph-theory-partitioners) instead of pushing from your main. It makes it way easier to go back and forth during reviews. Splitting the work into smaller commits (one for partitioning, one for conservation, one for visualization, etc) also helps a lot when reviewing. No big deal for this one though!

Really appreciate the effort here. Let me know if you have any questions about the comments. Happy to chat through any of them, and thank you so much for your time!😄

Comment on lines +89 to +93
def _calculate_equivalent(self, edges: list[dict[str, Any]], prop: str) -> float | None:
try:
return self._equivalent_reactance.aggregate_property(edges, prop)
except AggregationError:
return None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This silently returns None when the equivalent reactance calculation fails. The AggregationError is caught and swallowed with no logging. This could hide real problems (e.g., missing properties). Please add a log_warning when returning None due to a caught exception, so users can diagnose issues.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted _calculate_equivalent() so it logs a warning whenever AggregationError is caught before returning None. The warning includes the property name and exception so users see when impedance aggregation failed (e.g., missing attributes), rather than silently dropping the value.

Comment on lines +63 to +64
node_to_cluster = build_node_to_cluster_map(partition_map)
cluster_edge_map = build_cluster_edge_map(original_graph, node_to_cluster)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two mappings are already computed by AggregationManager.aggregate() in managers.py:272-273, but aren't passed down to the physical strategy. This means every call to TransformerConservationStrategy recomputes them from scratch.
The pre-computed maps could be forwarded via the parameters dict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now passing the pre-computed node_to_cluster and cluster_edge_map into the physical strategy via profile.physical_parameters before calling aggregate(). TransformerConservationStrategy uses those maps if provided, falling back to recomputing only if they’re missing, so we avoid redundant work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Minor note: the existing required_topology validation in managers.py:292 compares topology_strategy.__class__.__name__ against physical_strategy.required_topology, which means it compares "ElectricalTopologyStrategy" to "electrical". They will never match, producing a false warning every time CONSERVATION mode is used.

This is a pre-existing bug, but your PR is the first to trigger it. Could you fix the check in managers.py:292 to use profile.topology_strategy instead of topology_strategy.__class__.__name__?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: AggregationManager.aggregate() now compares profile.topology_strategy (string like "electrical") against physical_strategy.required_topology, so the warning only fires when the configured topology string actually differs from what the physical strategy expects. This prevents the false warning that appeared when running CONSERVATION mode.

Comment on lines +71 to +97
class CommunityPartitioning(PartitioningStrategy):
"""
Partition using greedy modularity community detection.

This strategy uses NetworkX's ``greedy_modularity_communities`` routine to detect communities
and is deterministic for a given graph structure.
"""

def required_attributes(self) -> dict[str, list[str]]:
return {"nodes": [], "edges": []}

def _strategy_name(self) -> str:
return "community_modularity"

def partition(self, graph: nx.DiGraph, **kwargs) -> dict[int, list[Any]]:
communities = list(greedy_modularity_communities(graph.to_undirected()))

if not communities:
raise PartitioningError(
"Community detection returned no communities.",
strategy=self._strategy_name(),
)

partition_map = {idx: list(cluster) for idx, cluster in enumerate(communities)}
validate_partition(partition_map, graph.number_of_nodes(), self._strategy_name())

return partition_map
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since greedy modularity determines the number of clusters automatically, what happens if a user passes n_clusters? It will be silently ignored. Please either emit a warning when n_clusters is provided (e.g., log_warning("community_modularity determines cluster count automatically, n_clusters is ignored")) or document this clearly in the docstring.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommunityPartitioning.partition() now checks for n_clusters in its kwargs and emits a log_warning stating that the modularity-based routine determines cluster count automatically, so n_clusters is ignored. The docstring also now calls this out. Tests cover the warning via caplog.

Comment on lines +14 to +27
class SpectralPartitioning(PartitioningStrategy):
"""
Partition using spectral clustering on the graph Laplacian.

This strategy converts the input graph into a symmetric adjacency matrix and applies
``sklearn.cluster.SpectralClustering`` with ``affinity="precomputed"``. Use this when
you need community-aware partitions on graphs that are not easily handled by geometric
distances (e.g., line topology with weak geographic structure).

Parameters
----------
random_state : int | None
Seed for reproducible k-means assignment inside spectral clustering.
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing partitioning strategies all document their AC-island awareness behavior. Could you add a note about how spectral clustering interacts with AC islands? Since it operates on the adjacency matrix, it should naturally respect graph connectivity, but it would be good to document this explicitly for consistency with the other strategies :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the requested note to SpectralPartitioning’s docstring: it now explicitly says the adjacency matrix is derived from the graph connectivity, so disconnected AC islands stay separate clusters and the strategy respects island boundaries automatically. This keeps it consistent with the other partitioners’ documentation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current plot_network() function creates a PlotConfig(**kwargs), but PlotConfig has no preset field. If a user passes preset=PlotPreset.PRESENTATION as shown in the workflows doc, this would raise a TypeError.

Please verify this works end-to-end. The preset likely needs to be popped from kwargs and applied before creating the PlotConfig.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the implementation now pops any preset (and accepts PlotPreset or string names) before building the PlotConfig, applies _apply_preset_overrides, and only then merges in the remaining kwargs. So calling plot_network(..., preset=PlotPreset.PRESENTATION) no longer fails, and the workflows example works end-to-end. The change has been tested via python -m pytest and python -m ruff check .

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 20, 2026

Codecov Report

❌ Patch coverage is 67.15328% with 45 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
npap/visualization.py 0.00% 31 Missing ⚠️
npap/aggregation/physical_strategies.py 85.10% 5 Missing and 2 partials ⚠️
npap/partitioning/graph_theory.py 86.36% 4 Missing and 2 partials ⚠️
npap/managers.py 88.88% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Author

@omari91 omari91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MarcoAnarmo have a look at this update for review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants