steinerpy 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- steinerpy-0.1.1/LICENSE +21 -0
- steinerpy-0.1.1/PKG-INFO +107 -0
- steinerpy-0.1.1/README.md +67 -0
- steinerpy-0.1.1/example.ipynb +325 -0
- steinerpy-0.1.1/pyproject.toml +99 -0
- steinerpy-0.1.1/requirements.txt +2 -0
- steinerpy-0.1.1/steinerpy/__init__.py +14 -0
- steinerpy-0.1.1/steinerpy/_version.py +3 -0
- steinerpy-0.1.1/steinerpy/mathematical_model.py +244 -0
- steinerpy-0.1.1/steinerpy/objects.py +49 -0
steinerpy-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Berend
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
steinerpy-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steinerpy
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Package to solve Steiner Tree and Steiner Forest Problems with the HiGHS solver
|
|
5
|
+
Project-URL: Homepage, https://github.com/berendmarkhorst/SteinerPy
|
|
6
|
+
Project-URL: Repository, https://github.com/berendmarkhorst/SteinerPy
|
|
7
|
+
Project-URL: Issues, https://github.com/berendmarkhorst/SteinerPy/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/berendmarkhorst/SteinerPy
|
|
9
|
+
Project-URL: Changelog, https://github.com/berendmarkhorst/SteinerPy/releases
|
|
10
|
+
Author: Berend Markhorst, Joost Berkhout, Alessandro Zocca, Jeroen Pruyn, Rob van der Mei
|
|
11
|
+
Maintainer-email: Berend Markhorst <your.email@example.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: forest,graph,highs,networkx,optimization,steiner,tree
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Requires-Dist: highspy
|
|
29
|
+
Requires-Dist: networkx
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black; extra == 'dev'
|
|
32
|
+
Requires-Dist: flake8; extra == 'dev'
|
|
33
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: sphinx; extra == 'docs'
|
|
38
|
+
Requires-Dist: sphinx-rtd-theme; extra == 'docs'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# SteinerPy
|
|
42
|
+
|
|
43
|
+
[](https://badge.fury.io/py/steinerpy)
|
|
44
|
+
[](https://www.python.org/downloads/)
|
|
45
|
+
[](https://opensource.org/licenses/MIT)
|
|
46
|
+
|
|
47
|
+
A Python package for solving Steiner Tree and Steiner Forest Problems using the HiGHS solver and NetworkX graphs.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
Install SteinerPy using pip:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install steinerpy
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or using uv:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv add steinerpy
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import networkx as nx
|
|
67
|
+
from steinerpy import SteinerProblem
|
|
68
|
+
|
|
69
|
+
# Create a graph
|
|
70
|
+
G = nx.Graph()
|
|
71
|
+
G.add_edge('A', 'B', weight=1)
|
|
72
|
+
G.add_edge('B', 'C', weight=2)
|
|
73
|
+
G.add_edge('C', 'D', weight=1)
|
|
74
|
+
|
|
75
|
+
# Define terminal groups
|
|
76
|
+
terminal_groups = [['A', 'D']]
|
|
77
|
+
|
|
78
|
+
# Solve the Steiner problem
|
|
79
|
+
problem = SteinerProblem(G, terminal_groups)
|
|
80
|
+
solution = problem.get_solution()
|
|
81
|
+
|
|
82
|
+
print(f"Optimal cost: {solution.objective}")
|
|
83
|
+
print(f"Selected edges: {solution.selected_edges}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Usage Examples
|
|
87
|
+
|
|
88
|
+
See the `example.ipynb` notebook for detailed usage examples.
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
- `networkx`: For graph representation and manipulation
|
|
93
|
+
- `highspy`: For optimization solving
|
|
94
|
+
|
|
95
|
+
If you use this package in your research, please cite:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
@article{markhorst2025future,
|
|
99
|
+
title={Future-proof ship pipe routing: Navigating the energy transition},
|
|
100
|
+
author={Markhorst, Berend and Berkhout, Joost and Zocca, Alessandro and Pruyn, Jeroen and van der Mei, Rob},
|
|
101
|
+
journal={Ocean Engineering},
|
|
102
|
+
volume={319},
|
|
103
|
+
pages={120113},
|
|
104
|
+
year={2025},
|
|
105
|
+
publisher={Elsevier}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# SteinerPy
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/steinerpy)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A Python package for solving Steiner Tree and Steiner Forest Problems using the HiGHS solver and NetworkX graphs.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install SteinerPy using pip:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install steinerpy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or using uv:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv add steinerpy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import networkx as nx
|
|
27
|
+
from steinerpy import SteinerProblem
|
|
28
|
+
|
|
29
|
+
# Create a graph
|
|
30
|
+
G = nx.Graph()
|
|
31
|
+
G.add_edge('A', 'B', weight=1)
|
|
32
|
+
G.add_edge('B', 'C', weight=2)
|
|
33
|
+
G.add_edge('C', 'D', weight=1)
|
|
34
|
+
|
|
35
|
+
# Define terminal groups
|
|
36
|
+
terminal_groups = [['A', 'D']]
|
|
37
|
+
|
|
38
|
+
# Solve the Steiner problem
|
|
39
|
+
problem = SteinerProblem(G, terminal_groups)
|
|
40
|
+
solution = problem.get_solution()
|
|
41
|
+
|
|
42
|
+
print(f"Optimal cost: {solution.objective}")
|
|
43
|
+
print(f"Selected edges: {solution.selected_edges}")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage Examples
|
|
47
|
+
|
|
48
|
+
See the `example.ipynb` notebook for detailed usage examples.
|
|
49
|
+
|
|
50
|
+
## Dependencies
|
|
51
|
+
|
|
52
|
+
- `networkx`: For graph representation and manipulation
|
|
53
|
+
- `highspy`: For optimization solving
|
|
54
|
+
|
|
55
|
+
If you use this package in your research, please cite:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
@article{markhorst2025future,
|
|
59
|
+
title={Future-proof ship pipe routing: Navigating the energy transition},
|
|
60
|
+
author={Markhorst, Berend and Berkhout, Joost and Zocca, Alessandro and Pruyn, Jeroen and van der Mei, Rob},
|
|
61
|
+
journal={Ocean Engineering},
|
|
62
|
+
volume={319},
|
|
63
|
+
pages={120113},
|
|
64
|
+
year={2025},
|
|
65
|
+
publisher={Elsevier}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "c0ac72cda79156ac",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": [
|
|
8
|
+
"# Example of solving a Steiner Tree/Forest Problem using HighsPy\n",
|
|
9
|
+
"This example demonstrates how to use the HighsPy library to solve a Steiner Tree Problem on a simple graph. We will create a graph with nodes and edges, define the terminals we want to connect, and then use the `SteinerProblem` class to find the optimal solution."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"cell_type": "code",
|
|
14
|
+
"execution_count": null,
|
|
15
|
+
"id": "initial_id",
|
|
16
|
+
"metadata": {
|
|
17
|
+
"ExecuteTime": {
|
|
18
|
+
"end_time": "2025-11-11T11:13:52.132433Z",
|
|
19
|
+
"start_time": "2025-11-11T11:13:51.793202Z"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"outputs": [],
|
|
23
|
+
"source": [
|
|
24
|
+
"import networkx as nx\n",
|
|
25
|
+
"from steinerpy import SteinerProblem"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"cell_type": "markdown",
|
|
30
|
+
"id": "2bd47e809382c559",
|
|
31
|
+
"metadata": {},
|
|
32
|
+
"source": [
|
|
33
|
+
"We make a simple example with four nodes and four edges. We want to connect A, B, and D. The optimal solution is AC, BC, and CD."
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"cell_type": "code",
|
|
38
|
+
"execution_count": 2,
|
|
39
|
+
"id": "a5981e94e5a3231f",
|
|
40
|
+
"metadata": {
|
|
41
|
+
"ExecuteTime": {
|
|
42
|
+
"end_time": "2025-11-11T11:13:52.222502Z",
|
|
43
|
+
"start_time": "2025-11-11T11:13:52.220962Z"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"outputs": [],
|
|
47
|
+
"source": [
|
|
48
|
+
"graph = nx.Graph()\n",
|
|
49
|
+
"edges = [(\"A\", \"C\"), (\"A\", \"D\"), (\"B\", \"C\"), (\"C\", \"D\")]\n",
|
|
50
|
+
"weights = [1, 10, 1, 1]\n",
|
|
51
|
+
"\n",
|
|
52
|
+
"for i, edge in enumerate(edges):\n",
|
|
53
|
+
" graph.add_edge(edge[0], edge[1])\n",
|
|
54
|
+
" graph.edges[edge][f\"weight\"] = weights[i]"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"cell_type": "markdown",
|
|
59
|
+
"id": "f856a6c8caf083cc",
|
|
60
|
+
"metadata": {},
|
|
61
|
+
"source": [
|
|
62
|
+
"We now create a `SteinerProblem` instance with the graph and the terminals we want to connect. We then call the `get_solution` method to solve the problem, specifying a time limit of 300 seconds."
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"cell_type": "code",
|
|
67
|
+
"execution_count": 3,
|
|
68
|
+
"id": "e456f414e3e492ef",
|
|
69
|
+
"metadata": {
|
|
70
|
+
"ExecuteTime": {
|
|
71
|
+
"end_time": "2025-11-11T11:13:52.241085Z",
|
|
72
|
+
"start_time": "2025-11-11T11:13:52.230882Z"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"outputs": [
|
|
76
|
+
{
|
|
77
|
+
"name": "stderr",
|
|
78
|
+
"output_type": "stream",
|
|
79
|
+
"text": [
|
|
80
|
+
"INFO:root:Building the model.\n",
|
|
81
|
+
"INFO:root:Model built in 0.00 seconds.\n",
|
|
82
|
+
"INFO:root:Started with running the model...\n",
|
|
83
|
+
"INFO:root:Runtime: 0.01 seconds\n"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "stdout",
|
|
88
|
+
"output_type": "stream",
|
|
89
|
+
"text": [
|
|
90
|
+
"Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms\n",
|
|
91
|
+
"MIP has 46 rows; 37 cols; 124 nonzeros; 21 integer variables (21 binary)\n",
|
|
92
|
+
"Coefficient ranges:\n",
|
|
93
|
+
" Matrix [1e+00, 1e+00]\n",
|
|
94
|
+
" Cost [1e+00, 1e+01]\n",
|
|
95
|
+
" Bound [1e+00, 1e+00]\n",
|
|
96
|
+
" RHS [1e+00, 1e+00]\n",
|
|
97
|
+
"Presolving model\n",
|
|
98
|
+
"30 rows, 23 cols, 78 nonzeros 0s\n",
|
|
99
|
+
"20 rows, 16 cols, 54 nonzeros 0s\n",
|
|
100
|
+
"11 rows, 7 cols, 29 nonzeros 0s\n",
|
|
101
|
+
"9 rows, 6 cols, 22 nonzeros 0s\n",
|
|
102
|
+
"5 rows, 5 cols, 12 nonzeros 0s\n",
|
|
103
|
+
"4 rows, 4 cols, 10 nonzeros 0s\n",
|
|
104
|
+
"Presolve reductions: rows 4(-42); columns 4(-33); nonzeros 10(-114) \n",
|
|
105
|
+
"Objective function is integral with scale 1\n",
|
|
106
|
+
"\n",
|
|
107
|
+
"Solving MIP model with:\n",
|
|
108
|
+
" 4 rows\n",
|
|
109
|
+
" 4 cols (4 binary, 0 integer, 0 implied int., 0 continuous, 0 domain fixed)\n",
|
|
110
|
+
" 10 nonzeros\n",
|
|
111
|
+
"\n",
|
|
112
|
+
"Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;\n",
|
|
113
|
+
" I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;\n",
|
|
114
|
+
" S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;\n",
|
|
115
|
+
" Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero\n",
|
|
116
|
+
"\n",
|
|
117
|
+
" Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work \n",
|
|
118
|
+
"Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time\n",
|
|
119
|
+
"\n",
|
|
120
|
+
" u 0 0 0 100.00% -inf 3 Large 0 0 0 0 0.0s\n",
|
|
121
|
+
" 1 0 1 100.00% 3 3 0.00% 0 0 0 0 0.0s\n",
|
|
122
|
+
"\n",
|
|
123
|
+
"Solving report\n",
|
|
124
|
+
" Status Optimal\n",
|
|
125
|
+
" Primal bound 3\n",
|
|
126
|
+
" Dual bound 3\n",
|
|
127
|
+
" Gap 0% (tolerance: 0.01%)\n",
|
|
128
|
+
" P-D integral 0\n",
|
|
129
|
+
" Solution status feasible\n",
|
|
130
|
+
" 3 (objective)\n",
|
|
131
|
+
" 0 (bound viol.)\n",
|
|
132
|
+
" 0 (int. viol.)\n",
|
|
133
|
+
" 0 (row viol.)\n",
|
|
134
|
+
" Timing 0.01\n",
|
|
135
|
+
" Max sub-MIP depth 0\n",
|
|
136
|
+
" Nodes 1\n",
|
|
137
|
+
" Repair LPs 0\n",
|
|
138
|
+
" LP iterations 0\n"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"source": [
|
|
143
|
+
"solution = SteinerProblem(graph, [[\"A\", \"B\", \"D\"]]).get_solution(time_limit=300)"
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"cell_type": "markdown",
|
|
148
|
+
"id": "98319bc0466946d4",
|
|
149
|
+
"metadata": {},
|
|
150
|
+
"source": [
|
|
151
|
+
"We can now access the selected edges in the optimal solution, as well as the optimality gap, runtime, and objective value."
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"cell_type": "code",
|
|
156
|
+
"execution_count": 4,
|
|
157
|
+
"id": "7f491b1adbb517f0",
|
|
158
|
+
"metadata": {
|
|
159
|
+
"ExecuteTime": {
|
|
160
|
+
"end_time": "2025-11-11T11:13:52.251404Z",
|
|
161
|
+
"start_time": "2025-11-11T11:13:52.248118Z"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"outputs": [
|
|
165
|
+
{
|
|
166
|
+
"data": {
|
|
167
|
+
"text/plain": [
|
|
168
|
+
"[('A', 'C'), ('C', 'B'), ('C', 'D')]"
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
"execution_count": 4,
|
|
172
|
+
"metadata": {},
|
|
173
|
+
"output_type": "execute_result"
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
"source": [
|
|
177
|
+
"solution.selected_edges"
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"cell_type": "code",
|
|
182
|
+
"execution_count": 5,
|
|
183
|
+
"id": "f05907e2ea4b70fe",
|
|
184
|
+
"metadata": {
|
|
185
|
+
"ExecuteTime": {
|
|
186
|
+
"end_time": "2025-11-11T11:13:52.261696Z",
|
|
187
|
+
"start_time": "2025-11-11T11:13:52.259726Z"
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"outputs": [
|
|
191
|
+
{
|
|
192
|
+
"name": "stdout",
|
|
193
|
+
"output_type": "stream",
|
|
194
|
+
"text": [
|
|
195
|
+
"Optimality gap: 0.00\n",
|
|
196
|
+
"Runtime: 0.01 sec\n",
|
|
197
|
+
"Objective: 3.00\n"
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"source": [
|
|
202
|
+
"print(f\"Optimality gap: {solution.gap:.2f}\")\n",
|
|
203
|
+
"print(f\"Runtime: {solution.runtime:.2f} sec\")\n",
|
|
204
|
+
"print(f\"Objective: {solution.objective:.2f}\")"
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"cell_type": "markdown",
|
|
209
|
+
"id": "ad25f45170b7a861",
|
|
210
|
+
"metadata": {},
|
|
211
|
+
"source": [
|
|
212
|
+
"In case you're interested in Steiner Forest Problems, you can define multiple sets of terminals to connect. For example, we can connect A and B in one set and C and D in another set."
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"cell_type": "code",
|
|
217
|
+
"execution_count": 7,
|
|
218
|
+
"id": "ca5995b84a21a188",
|
|
219
|
+
"metadata": {
|
|
220
|
+
"ExecuteTime": {
|
|
221
|
+
"end_time": "2025-11-11T11:18:35.817784Z",
|
|
222
|
+
"start_time": "2025-11-11T11:18:35.811777Z"
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"outputs": [
|
|
226
|
+
{
|
|
227
|
+
"name": "stderr",
|
|
228
|
+
"output_type": "stream",
|
|
229
|
+
"text": [
|
|
230
|
+
"INFO:root:Building the model.\n",
|
|
231
|
+
"INFO:root:Model built in 0.00 seconds.\n",
|
|
232
|
+
"INFO:root:Started with running the model...\n",
|
|
233
|
+
"INFO:root:Runtime: 0.00 seconds\n"
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "stdout",
|
|
238
|
+
"output_type": "stream",
|
|
239
|
+
"text": [
|
|
240
|
+
"Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms\n",
|
|
241
|
+
"MIP has 86 rows; 63 cols; 224 nonzeros; 31 integer variables (31 binary)\n",
|
|
242
|
+
"Coefficient ranges:\n",
|
|
243
|
+
" Matrix [1e+00, 1e+00]\n",
|
|
244
|
+
" Cost [1e+00, 1e+01]\n",
|
|
245
|
+
" Bound [1e+00, 1e+00]\n",
|
|
246
|
+
" RHS [1e+00, 1e+00]\n",
|
|
247
|
+
"Presolving model\n",
|
|
248
|
+
"50 rows, 37 cols, 121 nonzeros 0s\n",
|
|
249
|
+
"29 rows, 21 cols, 76 nonzeros 0s\n",
|
|
250
|
+
"22 rows, 15 cols, 55 nonzeros 0s\n",
|
|
251
|
+
"18 rows, 11 cols, 43 nonzeros 0s\n",
|
|
252
|
+
"14 rows, 10 cols, 32 nonzeros 0s\n",
|
|
253
|
+
"9 rows, 6 cols, 19 nonzeros 0s\n",
|
|
254
|
+
"6 rows, 4 cols, 13 nonzeros 0s\n",
|
|
255
|
+
"4 rows, 3 cols, 9 nonzeros 0s\n",
|
|
256
|
+
"1 rows, 2 cols, 2 nonzeros 0s\n",
|
|
257
|
+
"0 rows, 1 cols, 0 nonzeros 0s\n",
|
|
258
|
+
"0 rows, 0 cols, 0 nonzeros 0s\n",
|
|
259
|
+
"Presolve reductions: rows 0(-86); columns 0(-63); nonzeros 0(-224) - Reduced to empty\n",
|
|
260
|
+
"Presolve: Optimal\n",
|
|
261
|
+
"\n",
|
|
262
|
+
"Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;\n",
|
|
263
|
+
" I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;\n",
|
|
264
|
+
" S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;\n",
|
|
265
|
+
" Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero\n",
|
|
266
|
+
"\n",
|
|
267
|
+
" Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work \n",
|
|
268
|
+
"Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time\n",
|
|
269
|
+
"\n",
|
|
270
|
+
" 0 0 0 0.00% 3 3 0.00% 0 0 0 0 0.0s\n",
|
|
271
|
+
"\n",
|
|
272
|
+
"Solving report\n",
|
|
273
|
+
" Status Optimal\n",
|
|
274
|
+
" Primal bound 3\n",
|
|
275
|
+
" Dual bound 3\n",
|
|
276
|
+
" Gap 0% (tolerance: 0.01%)\n",
|
|
277
|
+
" P-D integral 0\n",
|
|
278
|
+
" Solution status feasible\n",
|
|
279
|
+
" 3 (objective)\n",
|
|
280
|
+
" 0 (bound viol.)\n",
|
|
281
|
+
" 0 (int. viol.)\n",
|
|
282
|
+
" 0 (row viol.)\n",
|
|
283
|
+
" Timing 0.00\n",
|
|
284
|
+
" Max sub-MIP depth 0\n",
|
|
285
|
+
" Nodes 0\n",
|
|
286
|
+
" Repair LPs 0\n",
|
|
287
|
+
" LP iterations 0\n"
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"source": [
|
|
292
|
+
"solution = SteinerProblem(graph, [[\"A\", \"B\"], [\"C\", \"D\"]]).get_solution(time_limit=300)\n"
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"cell_type": "code",
|
|
297
|
+
"execution_count": null,
|
|
298
|
+
"id": "4284e34a175c3289",
|
|
299
|
+
"metadata": {},
|
|
300
|
+
"outputs": [],
|
|
301
|
+
"source": []
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
"metadata": {
|
|
305
|
+
"kernelspec": {
|
|
306
|
+
"display_name": "Python 3",
|
|
307
|
+
"language": "python",
|
|
308
|
+
"name": "python3"
|
|
309
|
+
},
|
|
310
|
+
"language_info": {
|
|
311
|
+
"codemirror_mode": {
|
|
312
|
+
"name": "ipython",
|
|
313
|
+
"version": 2
|
|
314
|
+
},
|
|
315
|
+
"file_extension": ".py",
|
|
316
|
+
"mimetype": "text/x-python",
|
|
317
|
+
"name": "python",
|
|
318
|
+
"nbconvert_exporter": "python",
|
|
319
|
+
"pygments_lexer": "ipython2",
|
|
320
|
+
"version": "2.7.6"
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
"nbformat": 4,
|
|
324
|
+
"nbformat_minor": 5
|
|
325
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "steinerpy"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Package to solve Steiner Tree and Steiner Forest Problems with the HiGHS solver"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Berend Markhorst"},
|
|
13
|
+
{name = "Joost Berkhout"},
|
|
14
|
+
{name = "Alessandro Zocca"},
|
|
15
|
+
{name = "Jeroen Pruyn"},
|
|
16
|
+
{name = "Rob van der Mei"},
|
|
17
|
+
]
|
|
18
|
+
maintainers = [
|
|
19
|
+
{name = "Berend Markhorst", email = "your.email@example.com"}, # Replace with actual email
|
|
20
|
+
]
|
|
21
|
+
keywords = ["steiner", "tree", "forest", "optimization", "graph", "networkx", "highs"]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.8",
|
|
29
|
+
"Programming Language :: Python :: 3.9",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Topic :: Scientific/Engineering",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
35
|
+
]
|
|
36
|
+
requires-python = ">=3.8"
|
|
37
|
+
dependencies = [
|
|
38
|
+
"networkx",
|
|
39
|
+
"highspy",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest",
|
|
45
|
+
"pytest-cov",
|
|
46
|
+
"black",
|
|
47
|
+
"flake8",
|
|
48
|
+
"mypy",
|
|
49
|
+
]
|
|
50
|
+
docs = [
|
|
51
|
+
"sphinx",
|
|
52
|
+
"sphinx-rtd-theme",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://github.com/berendmarkhorst/SteinerPy"
|
|
57
|
+
Repository = "https://github.com/berendmarkhorst/SteinerPy"
|
|
58
|
+
Issues = "https://github.com/berendmarkhorst/SteinerPy/issues"
|
|
59
|
+
Documentation = "https://github.com/berendmarkhorst/SteinerPy"
|
|
60
|
+
Changelog = "https://github.com/berendmarkhorst/SteinerPy/releases"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.wheel]
|
|
63
|
+
packages = ["steinerpy"]
|
|
64
|
+
|
|
65
|
+
[tool.hatch.build.targets.sdist]
|
|
66
|
+
include = [
|
|
67
|
+
"/steinerpy",
|
|
68
|
+
"/README.md",
|
|
69
|
+
"/LICENSE",
|
|
70
|
+
"/requirements.txt",
|
|
71
|
+
"/example.ipynb",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.black]
|
|
75
|
+
line-length = 88
|
|
76
|
+
target-version = ['py38']
|
|
77
|
+
include = '\.pyi?$'
|
|
78
|
+
extend-exclude = '''
|
|
79
|
+
/(
|
|
80
|
+
# directories
|
|
81
|
+
\.eggs
|
|
82
|
+
| \.git
|
|
83
|
+
| \.hg
|
|
84
|
+
| \.mypy_cache
|
|
85
|
+
| \.tox
|
|
86
|
+
| \.venv
|
|
87
|
+
| _build
|
|
88
|
+
| buck-out
|
|
89
|
+
| build
|
|
90
|
+
| dist
|
|
91
|
+
)/
|
|
92
|
+
'''
|
|
93
|
+
|
|
94
|
+
[tool.pytest.ini_options]
|
|
95
|
+
testpaths = ["tests"]
|
|
96
|
+
python_files = "test_*.py"
|
|
97
|
+
python_classes = "Test*"
|
|
98
|
+
python_functions = "test_*"
|
|
99
|
+
addopts = "-v --cov=src --cov-report=html --cov-report=term-missing"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SteinerPy: A Python package for solving Steiner Tree and Steiner Forest Problems.
|
|
3
|
+
|
|
4
|
+
This package provides tools to solve Steiner Tree and Steiner Forest problems
|
|
5
|
+
using the HiGHS solver with NetworkX graphs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .objects import SteinerProblem, Solution
|
|
9
|
+
from .mathematical_model import build_model, run_model
|
|
10
|
+
from ._version import __version__
|
|
11
|
+
__author__ = "Berend Markhorst, Joost Berkhout, Alessandro Zocca, Jeroen Pruyn, Rob van der Mei"
|
|
12
|
+
__email__ = "berend.markhorst@cwi.nl"
|
|
13
|
+
|
|
14
|
+
__all__ = ["SteinerProblem", "Solution", "build_model", "run_model"]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import highspy as hp
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
# Configure logging
|
|
6
|
+
logging.basicConfig(level=logging.INFO)
|
|
7
|
+
|
|
8
|
+
def make_model(time_limit: float, logfile: str = "") -> hp.HighsModel:
|
|
9
|
+
"""
|
|
10
|
+
Creates a HiGHS model with the given time limit and logfile.
|
|
11
|
+
:param time_limit: time limit in seconds for the HiGHS model.
|
|
12
|
+
:param logfile: path to logfile.
|
|
13
|
+
:return: HiGHS model.
|
|
14
|
+
"""
|
|
15
|
+
# Create model
|
|
16
|
+
model = hp.Highs()
|
|
17
|
+
model.setOptionValue("time_limit", time_limit)
|
|
18
|
+
model.setOptionValue("output_flag", False) # Disable/enable console output
|
|
19
|
+
|
|
20
|
+
# Clear the logfile and start logging
|
|
21
|
+
if logfile != "":
|
|
22
|
+
with open(logfile, "w") as _:
|
|
23
|
+
pass
|
|
24
|
+
model.setOptionValue("log_file", logfile)
|
|
25
|
+
|
|
26
|
+
return model
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_terminals(terminal_group: list[list]) -> list:
|
|
30
|
+
"""
|
|
31
|
+
Turns a nested list of terminals into a list of terminals.
|
|
32
|
+
:param terminal_group: nested list of terminals.
|
|
33
|
+
:return: list of terminals.
|
|
34
|
+
"""
|
|
35
|
+
return [t for group in terminal_group for t in group]
|
|
36
|
+
|
|
37
|
+
def terminal_groups_without_root(terminal_group: list[list], roots: list, group_index: int) -> set:
|
|
38
|
+
"""
|
|
39
|
+
Get terminal groups until index k without kth root.
|
|
40
|
+
:param terminal_group: nested list of terminals.
|
|
41
|
+
:param roots: list of roots.
|
|
42
|
+
:param group_index: index of the terminal group.
|
|
43
|
+
:return: subset of terminal groups from index k to K.
|
|
44
|
+
"""
|
|
45
|
+
if len(terminal_group[0]) > 0:
|
|
46
|
+
return set(get_terminals(terminal_group[group_index:])) - set([roots[group_index]])
|
|
47
|
+
else:
|
|
48
|
+
return set()
|
|
49
|
+
|
|
50
|
+
def get_terminal_groups_until_k(terminal_groups: list[list], group_index: int) -> set:
|
|
51
|
+
"""
|
|
52
|
+
Get terminal groups until index k.
|
|
53
|
+
:param terminal_groups: nested list of terminals.
|
|
54
|
+
:param group_index: index of the terminal group.
|
|
55
|
+
:return: subset of terminal groups up till index k.
|
|
56
|
+
"""
|
|
57
|
+
return set(get_terminals(terminal_groups[:group_index]))
|
|
58
|
+
|
|
59
|
+
def add_directed_constraints(model: hp.HighsModel, steiner_problem: 'SteinerProblem') -> tuple[hp.HighsModel, dict[hp.HighsVarType]]:
|
|
60
|
+
"""
|
|
61
|
+
Adds DO-D constraints to the model (see Markhorst et al. 2025)
|
|
62
|
+
:param model: HiGHS model.
|
|
63
|
+
:param steiner_problem: AutomatedPipeRouting-object.
|
|
64
|
+
:return: HiGHS model with DO-D constraints and decision variables.
|
|
65
|
+
"""
|
|
66
|
+
# Sets
|
|
67
|
+
group_indices = range(len(steiner_problem.terminal_groups))
|
|
68
|
+
k_indices = [(k, l) for k in group_indices for l in group_indices if l >= k]
|
|
69
|
+
|
|
70
|
+
# Decision variables
|
|
71
|
+
x = {e: model.addVariable(0, 1, name=f"x[{e}]") for e in steiner_problem.edges}
|
|
72
|
+
y1 = {a: model.addVariable(0, 1, name=f"y1[{a}]") for a in steiner_problem.arcs}
|
|
73
|
+
y2 = {(group_id, a): model.addVariable(0, 1, name=f"y2[{group_id},{a}]") for group_id in group_indices
|
|
74
|
+
for a in steiner_problem.arcs}
|
|
75
|
+
z = {(k, l): model.addVariable(0, 1, name=f"z[{k},{l}]") for k, l in k_indices}
|
|
76
|
+
|
|
77
|
+
for col in range(model.getNumCol()):
|
|
78
|
+
model.changeColIntegrality(col, hp.HighsVarType.kInteger)
|
|
79
|
+
|
|
80
|
+
# Constraint 1: connection between y2 and y1
|
|
81
|
+
for group_id in group_indices:
|
|
82
|
+
for a in steiner_problem.arcs:
|
|
83
|
+
model.addConstr(y2[group_id, a] <= y1[a])
|
|
84
|
+
|
|
85
|
+
# Constraint 2: indegree of each vertex cannot exceed 1
|
|
86
|
+
for v in steiner_problem.nodes:
|
|
87
|
+
lhs = sum(y1[(u, w)] for u, w in steiner_problem.arcs if v == w)
|
|
88
|
+
model.addConstr(lhs <= 1)
|
|
89
|
+
|
|
90
|
+
# Constraint 3: connection between y1 and x
|
|
91
|
+
for u, v in steiner_problem.edges:
|
|
92
|
+
model.addConstr(y1[(u, v)] + y1[(v, u)] <= x[(u, v)])
|
|
93
|
+
|
|
94
|
+
# Constraint 4: enforce terminal group rooted at one root
|
|
95
|
+
for group_id_k in group_indices:
|
|
96
|
+
model.addConstr(sum(z[group_id_l, group_id_k] for group_id_l in group_indices
|
|
97
|
+
if group_id_l <= group_id_k) == 1)
|
|
98
|
+
|
|
99
|
+
# Constraint 5: enforce one root per arborescence
|
|
100
|
+
for group_id_k in group_indices:
|
|
101
|
+
for group_id_l in group_indices:
|
|
102
|
+
if group_id_l > group_id_k:
|
|
103
|
+
model.addConstr(z[group_id_k, group_id_k] >= z[group_id_k, group_id_l])
|
|
104
|
+
|
|
105
|
+
# Constraint 6: terminals in T^{1···k−1} cannot attach to root r k
|
|
106
|
+
for group_id_k in group_indices:
|
|
107
|
+
for t in get_terminal_groups_until_k(steiner_problem.terminal_groups, group_id_k):
|
|
108
|
+
lhs = sum(y2[group_id_k, a] for a in steiner_problem.arcs if a[1] == t)
|
|
109
|
+
model.addConstr(lhs == 0)
|
|
110
|
+
|
|
111
|
+
# Constraint 7: indegree at most outdegree for Steiner points
|
|
112
|
+
for v in steiner_problem.steiner_points:
|
|
113
|
+
model.addConstr(sum(y1[(a[0], a[1])] for a in steiner_problem.arcs if a[1] == v) <=
|
|
114
|
+
sum(y1[(a[0], a[1])] for a in steiner_problem.arcs if a[0] == v))
|
|
115
|
+
|
|
116
|
+
# Constraint 8: indegree at most outdegree per terminal group
|
|
117
|
+
for group_id_k in group_indices:
|
|
118
|
+
remaining_vertices = set(steiner_problem.nodes) - set(terminal_groups_without_root(steiner_problem.terminal_groups, steiner_problem.roots, group_id_k))
|
|
119
|
+
for v in remaining_vertices:
|
|
120
|
+
model.addConstr(sum(y2[group_id_k, (a[0], v)] for a in steiner_problem.arcs if a[1] == v) <=
|
|
121
|
+
sum(y2[group_id_k, (v, a[1])] for a in steiner_problem.arcs if a[0] == v))
|
|
122
|
+
|
|
123
|
+
# Constraint 9: connect y2 and z
|
|
124
|
+
for group_id_k in group_indices:
|
|
125
|
+
for group_id_l in group_indices:
|
|
126
|
+
if group_id_l > group_id_k:
|
|
127
|
+
model.addConstr(sum(y2[group_id_k, a] for a in steiner_problem.arcs if a[1] == steiner_problem.roots[group_id_l]) <= z[group_id_k, group_id_l])
|
|
128
|
+
|
|
129
|
+
return model, x, y1, y2, z
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def demand_and_supply_directed(steiner_problem: 'SteinerProblem', group_id_k: int, t: tuple, v: tuple, z: hp.HighsVarType) -> hp.HighsVarType | int:
|
|
133
|
+
"""
|
|
134
|
+
Calculate the demand and supply for a directed model.
|
|
135
|
+
:param cc_k: The current connected component.
|
|
136
|
+
:param t: A terminal represented as a tuple of integers.
|
|
137
|
+
:param v: A vertex represented as a tuple of integers.
|
|
138
|
+
:param z: The decision variable z.
|
|
139
|
+
:return: The value of z if the vertex is the root, -z if the vertex is a terminal, and 0 otherwise.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
# We assume terminals are disjoint from each other
|
|
143
|
+
group_id_l = [group_id for group_id, group in enumerate(steiner_problem.terminal_groups) if t in group][0]
|
|
144
|
+
|
|
145
|
+
if v == steiner_problem.roots[group_id_k]:
|
|
146
|
+
return z[(group_id_k, group_id_l)]
|
|
147
|
+
elif v == t:
|
|
148
|
+
return -z[(group_id_k, group_id_l)]
|
|
149
|
+
else:
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def add_flow_constraints(model: hp.HighsModel, steiner_problem: 'SteinerProblem', z: hp.HighsVarType, y2: hp.HighsVarType) -> tuple[hp.HighsModel, dict[hp.HighsVarType]]:
|
|
154
|
+
"""
|
|
155
|
+
We add the flow constraints to the HiGHS model.
|
|
156
|
+
:param model: HiGHS model.
|
|
157
|
+
:param steiner_problem: SteinerProblem-object.
|
|
158
|
+
:param z: decision variable z.
|
|
159
|
+
:param y2: decision variable y2.
|
|
160
|
+
:return: HiGHS model and variable(s).
|
|
161
|
+
"""
|
|
162
|
+
# Decision variables (binary flow variables)
|
|
163
|
+
group_indices = range(len(steiner_problem.terminal_groups))
|
|
164
|
+
f = {(group_id, t, a): model.addVariable(0, 1, hp.HighsVarType.kInteger, name=f"f[{group_id},{a}]") for group_id in group_indices
|
|
165
|
+
for t in terminal_groups_without_root(steiner_problem.terminal_groups, steiner_problem.roots, group_id) for a in steiner_problem.arcs}
|
|
166
|
+
|
|
167
|
+
# Constraint 1: flow conservation
|
|
168
|
+
for v in steiner_problem.nodes:
|
|
169
|
+
for group_id in group_indices:
|
|
170
|
+
for t in terminal_groups_without_root(steiner_problem.terminal_groups, steiner_problem.roots, group_id):
|
|
171
|
+
first_term = sum(f[group_id, t, a] for a in steiner_problem.arcs if a[0] == v)
|
|
172
|
+
second_term = sum(f[group_id, t, a] for a in steiner_problem.arcs if a[1] == v)
|
|
173
|
+
left_hand_side = first_term - second_term
|
|
174
|
+
demand_and_supply = demand_and_supply_directed(steiner_problem, group_id, t, v, z)
|
|
175
|
+
model.addConstr(left_hand_side == demand_and_supply)
|
|
176
|
+
|
|
177
|
+
# Constraint 2: connection between f and y2
|
|
178
|
+
for group_id in group_indices:
|
|
179
|
+
for t in terminal_groups_without_root(steiner_problem.terminal_groups, steiner_problem.roots, group_id):
|
|
180
|
+
for a in steiner_problem.arcs:
|
|
181
|
+
left_hand_side = f[group_id, t, a]
|
|
182
|
+
right_hand_side = y2[group_id, a]
|
|
183
|
+
model.addConstr(left_hand_side <= right_hand_side)
|
|
184
|
+
|
|
185
|
+
# Constraint 3: prevent flow from leaving a terminal
|
|
186
|
+
for group_id in group_indices:
|
|
187
|
+
for t in terminal_groups_without_root(steiner_problem.terminal_groups, steiner_problem.roots, group_id):
|
|
188
|
+
if sum(1 for u, v in steiner_problem.arcs if u == t) > 0:
|
|
189
|
+
left_hand_side = sum(f[group_id, t, (u, v)] for u, v in steiner_problem.arcs if u == t)
|
|
190
|
+
model.addConstr(left_hand_side == 0, name="flow_3")
|
|
191
|
+
|
|
192
|
+
return model, f
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def build_model(steiner_problem: 'SteinerProblem', time_limit: float = 300, logfile: str = "") -> tuple[hp.HighsModel, float]:
|
|
196
|
+
"""
|
|
197
|
+
Returns the deterministic directed model.
|
|
198
|
+
:param steiner_problem: SteinerProblem-object.
|
|
199
|
+
:param time_limit: time limit in seconds for the HiGHS model. Default is 300 seconds.
|
|
200
|
+
:param logfile: path to logfile.
|
|
201
|
+
:return: HiGHS model.
|
|
202
|
+
"""
|
|
203
|
+
# Create the model
|
|
204
|
+
logging.info("Building the model.")
|
|
205
|
+
|
|
206
|
+
model = make_model(time_limit, logfile)
|
|
207
|
+
|
|
208
|
+
# Start tracking compilation time
|
|
209
|
+
start_time = time.time()
|
|
210
|
+
|
|
211
|
+
# Add constraints
|
|
212
|
+
model, x, y1, y2, z = add_directed_constraints(model, steiner_problem)
|
|
213
|
+
model, f = add_flow_constraints(model, steiner_problem, z, y2)
|
|
214
|
+
|
|
215
|
+
# End tracking compilation time
|
|
216
|
+
end_time = time.time()
|
|
217
|
+
compilation_time = end_time - start_time
|
|
218
|
+
|
|
219
|
+
logging.info(f"Model built in {compilation_time:.2f} seconds.")
|
|
220
|
+
|
|
221
|
+
return model, x, y1, y2, z, f
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def run_model(model: hp.HighsModel, steiner_problem: 'SteinerProblem', x: hp.HighsVarType) -> tuple[float, float, float, list[tuple]]:
|
|
225
|
+
"""
|
|
226
|
+
Solves the model and returns the result.
|
|
227
|
+
:param model: highspy model.
|
|
228
|
+
:param steiner_problem: SteinerProblem-object.
|
|
229
|
+
:param x: highspy variable.
|
|
230
|
+
:return: Solution-object.
|
|
231
|
+
"""
|
|
232
|
+
logging.info(f"Started with running the model...")
|
|
233
|
+
|
|
234
|
+
# Optimize model
|
|
235
|
+
model.minimize(sum(x[e] * steiner_problem.graph.edges[e][steiner_problem.weight] for e in steiner_problem.edges))
|
|
236
|
+
|
|
237
|
+
logging.info(f"Runtime: {model.getRunTime():.2f} seconds")
|
|
238
|
+
|
|
239
|
+
selected_edges = [e for e in steiner_problem.edges if model.variableValue(x[e]) > 0.5]
|
|
240
|
+
gap = model.getInfo().mip_gap
|
|
241
|
+
runtime = model.getRunTime()
|
|
242
|
+
objective = model.getObjectiveValue()
|
|
243
|
+
|
|
244
|
+
return gap, runtime, objective, selected_edges
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
import highspy as hp
|
|
3
|
+
from .mathematical_model import build_model, run_model
|
|
4
|
+
|
|
5
|
+
class SteinerProblem:
|
|
6
|
+
def __init__(self, graph: nx.Graph, terminal_groups: list[list], weight="weight"):
|
|
7
|
+
"""
|
|
8
|
+
Initialize the SteinerProblem (can be tree or forest).
|
|
9
|
+
:param graph: networkx graph.
|
|
10
|
+
:param terminal_groups: nested list of terminals.
|
|
11
|
+
:param weight: edge attribute specified by this string as the edge weight.
|
|
12
|
+
"""
|
|
13
|
+
self.graph = graph
|
|
14
|
+
self.terminal_groups = terminal_groups
|
|
15
|
+
self.weight = weight
|
|
16
|
+
self.edges = list(self.graph.edges())
|
|
17
|
+
self.arcs = self.edges + [(v, u) for (u, v) in self.edges]
|
|
18
|
+
self.nodes = list(self.graph.nodes())
|
|
19
|
+
self.steiner_points = set(self.nodes) - set([t for group in terminal_groups for t in group])
|
|
20
|
+
self.roots = [group[0] for group in self.terminal_groups]
|
|
21
|
+
|
|
22
|
+
def __repr__(self):
|
|
23
|
+
return f"Problem with a graph of {self.graph.number_of_nodes()} nodes and {self.graph.number_of_edges()} edges and {self.terminal_groups} as terminal groups."
|
|
24
|
+
|
|
25
|
+
def get_solution(self, time_limit: float = 300, log_file: str = "") -> 'Solution':
|
|
26
|
+
"""
|
|
27
|
+
Get the solution of the Steiner Problem using HighsPy.
|
|
28
|
+
:param time_limit: time limit in seconds.
|
|
29
|
+
:param log_file: path to the log file.
|
|
30
|
+
:return: Solution object.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
model, x, y1, y2, z, f = build_model(self, time_limit=time_limit, logfile=log_file)
|
|
34
|
+
|
|
35
|
+
gap, runtime, objective, selected_edges = run_model(model, self, x)
|
|
36
|
+
|
|
37
|
+
solution = Solution(gap, runtime, objective, selected_edges)
|
|
38
|
+
|
|
39
|
+
return solution
|
|
40
|
+
|
|
41
|
+
class Solution:
|
|
42
|
+
def __init__(self, gap: float, runtime: float, objective: float, selected_edges: list[tuple]):
|
|
43
|
+
self.gap = gap
|
|
44
|
+
self.runtime = runtime
|
|
45
|
+
self.objective = objective
|
|
46
|
+
self.selected_edges = selected_edges
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|