Skip to content

Add tree-decomposition-based partitioning.#149

Open
samuelsonric wants to merge 5 commits into
plasmo-dev:mainfrom
samuelsonric:main
Open

Add tree-decomposition-based partitioning.#149
samuelsonric wants to merge 5 commits into
plasmo-dev:mainfrom
samuelsonric:main

Conversation

@samuelsonric
Copy link
Copy Markdown

@samuelsonric samuelsonric commented Mar 5, 2026

This PR adds adds a partitioning feature using CliqueTrees.jl. The fuction apply_clique_tree! transforms an opti-graph into a forest of disjoint subgraphs, which can be used by PlasmoBenders.jl.

Example

using Plasmo, PlasmoBenders, HiGHS, JuMP                                                                                                   

# Petersen graph
function build_graph()                      
      graph = OptiGraph()

      @optinode(graph, n[0:9])                                                                                                                                     
      @variable(n[0], 0 <= cap <= 10, Int)
      @variable(n[0], 0 <= x)       
      @constraint(n[0], x <= 5cap)  
      @objective(n[0], Min, 30cap + 2x)         
     
      for i in 1:9        
          @variable(n[i], 0 <= x)                     
          @variable(n[i], 0 <= slack)        
          @constraint(n[i], x + slack >= 5 + i)                    
          @objective(n[i], Min, 3x + 100slack) 
      end      
    
      for (i, j) in [(0,1), (1,2), (2,3), (3,4), (4,0)]       
          @linkconstraint(graph, n[i][:x] + n[j][:x] >= 8)                         
      end           
     
      for (i, j) in [(5,7), (7,9), (9,6), (6,8), (8,5)]                
          @linkconstraint(graph, n[i][:x] + n[j][:x] >= 7)
      end  

      for (i, j) in [(0,5), (1,6), (2,7), (3,8), (4,9)]                 
          @linkconstraint(graph, n[i][:x] + n[j][:x] >= 6)     
      end 
       
      set_to_node_objectives(graph)   
      return graph
end

function solve_base(solver, graph)
    set_optimizer(graph, solver)
    optimize!(graph)
    return objective_value(graph)
end

function solve_benders(solver, graph; kw...)
    root = apply_clique_tree!(graph; kw...)

    for subgraph in local_subgraphs(graph)              
      set_optimizer(subgraph, solver)
    end

    benders = BendersAlgorithm(graph, root; max_iters=50, solver=solver)                                                                 
    run_algorithm!(benders)
    return benders.best_upper_bound
end
            
solver = optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false)

We can solve this naively...

julia> solve_base(solver, build_graph())
275.999999

... or using nested Benders.

julia> solve_benders(solver, build_graph())
Initializing BendersAlgorithm...
BendersAlgorithm Initialized!
################################################
Running BendersAlgorithm
################################################

Number of Variables: 72
Number of Subproblems: 5

Iter |      Gap | LowerBound | UpperBound | Time (s)
   1 |    70.7 % | 8.100e+01 | 2.760e+02 | 1.099e-02
   2 |      37 % | 1.740e+02 | 2.760e+02 | 9.408e-03
   3 |       0 % | 2.760e+02 | 2.760e+02 | 9.465e-03
Optimal Solution Found!
275.999999

@samuelsonric
Copy link
Copy Markdown
Author

@dlcole3

@dlcole3
Copy link
Copy Markdown
Collaborator

dlcole3 commented Apr 15, 2026

@samuelsonric apologies for the delay. Thank you for opening the PR. This looks very interesting. I am looping in @jalving here. I am going to be unavailable until the end of April, but I will review it within the week I get back if @jalving does not get to it first.

@dlcole3 dlcole3 requested review from jalving and removed request for jalving April 30, 2026 13:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new clique-tree (tree-decomposition) based partitioning workflow to transform an OptiGraph into a forest of subgraphs, intended for downstream decomposition algorithms (e.g., PlasmoBenders.jl).

Changes:

  • Adds apply_clique_tree! and supporting logic (CliqueTrees.jl integration) to build clique-tree bags and rewrite/link constraints across bags.
  • Exposes the new API from the main module and wires it into the build via include.
  • Adds CliqueTrees.jl as a dependency and bumps the minimum supported Julia version to 1.10 (plus updates the docs workflow Julia version).

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/Plasmo.jl Exports apply_clique_tree! and includes the new clique-tree implementation file.
src/graph_functions/cliquetree.jl Implements clique-tree construction and an in-place graph transformation into bag subgraphs with separator-linking constraints.
Project.toml Adds CliqueTrees dependency/compat and raises julia compat to 1.10.
.github/workflows/Documentation.yml Updates docs build to use Julia 1.10.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/graph_functions/cliquetree.jl Outdated
Comment on lines +83 to +88
optinodes = all_nodes(graph)[perm]
optiedges = all_edges(graph)

empty!(graph.optinodes)
empty!(graph.optiedges)

Comment on lines +134 to +138
for (e, j) in enumerate(edge_to_bag)
for con in JuMP.all_constraints(optiedges[e])
con_obj = JuMP.constraint_object(con)
_add_substituted_constraint!(con_obj, bag_to_graph[j], bag_to_var_map[j])
end
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.

If the clique decomposition truly requires scalar constraints, I would suggest doing the check early on for whether any edges have nonlinear constraints. I think there is even a Plasmo method to do this.

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 clique decomposition does not require scalar constraints.

Comment thread src/graph_functions/cliquetree.jl Outdated
Comment on lines +145 to +153
"""
apply_clique_tree!(graph::OptiGraph; kw...)

Transform an opti-graph into a tree of subgraphs.
"""
function apply_clique_tree!(graph::OptiGraph; kw...)
perm, tree, edge_to_bag = opti_clique_tree(graph; kw...)
return apply_clique_tree!(graph, perm, tree, edge_to_bag)
end
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.

At least one good unit test would great!

samuelsonric and others added 2 commits May 17, 2026 12:07
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@jalving
Copy link
Copy Markdown
Member

jalving commented May 17, 2026

Hey @samuelsonric, sorry this took so long to address. Thank you for your contribution!

I am wondering if it is actually possible to generate a Partition object in graph_functions/partition.jl from the tree you generate, or if that is insufficient for the decomposition (for instance, you require very custom constraint handling to build the new graph). I don't think you need to go this route, but worth looking into and making an issue if it seems possible. Since your code is isolated, it should be fine as is.

My one request is to try adding CliqueTrees.jl to the init function the same way KaHyPar is handled. This avoids requiring it as a dependency for Plasmo.jl.

@samuelsonric
Copy link
Copy Markdown
Author

samuelsonric commented May 17, 2026

Hello @jalving @dlcole3 I will address these comments today. Here is a visual description of what this code is doing in the given example. Starting with an OptiGraph shaped like the Petersen graph...

graph

...we form a supergraph by adding extra vertices (red). We then partition the vertices of this supergraph, forming a tree of subgraphs.

tree

@samuelsonric
Copy link
Copy Markdown
Author

samuelsonric commented May 17, 2026

@jalving There is a rough correspondence between tree decompositions and hierarchical edge-cuts (your Partition objects). Recall that each OptiGraph generates two hypergraphs:

  • $H$: hyper_projection
  • $H^\mathsf{T}$: edge_hyper_projection

A tree decomposition of $H$ corresponds to a hierarchical edge-cut of $H^\mathsf{T}$, and a tree decomposition of $H^\mathsf{T}$ corresponds to a hierarchical edge-cut of $H$. This correspondence is not one-to-one, but each thing can be constructed from the other thing.

The code in this PR computes a tree decomposition of $H$, and hence a hierarchical edge-cut of $H^\mathsf{T}$. This differs from your existing partition infrastructure, which forms hierarchical edge-cuts of $H$.

I chose the former because it produces OptiGraphs more amenable to Bender's decomposition.

@samuelsonric
Copy link
Copy Markdown
Author

samuelsonric commented May 17, 2026

TD;DR I am partitioning hyperedges (constraints), wheras you partition vertices (variables).

I seem to have made some assumptions. You do have the ability to partition $H^\mathsf{T}$...

@dlcole3
Copy link
Copy Markdown
Collaborator

dlcole3 commented May 18, 2026

My one request is to try adding CliqueTrees.jl to the init function the same way KaHyPar is handled. This avoids requiring it as a dependency for Plasmo.jl.

I had updated the Project.toml to use CliqueTrees.jl as a dependency, but I think we should remove it then and just have it in the init section.

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.

4 participants