simplinho 0.1.0__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.
- simplinho-0.1.0/.gitignore +18 -0
- simplinho-0.1.0/CMakeLists.txt +77 -0
- simplinho-0.1.0/LICENSE +21 -0
- simplinho-0.1.0/PKG-INFO +302 -0
- simplinho-0.1.0/README.md +260 -0
- simplinho-0.1.0/assets/simplinho-logo.svg +70 -0
- simplinho-0.1.0/bindings/simplex_bindings.cpp +1877 -0
- simplinho-0.1.0/include/simplex/aux.h +23 -0
- simplinho-0.1.0/include/simplex/degeneracy.h +336 -0
- simplinho-0.1.0/include/simplex/presolver.h +1539 -0
- simplinho-0.1.0/include/simplex/pricer.h +1079 -0
- simplinho-0.1.0/include/simplex/simplex.h +2730 -0
- simplinho-0.1.0/include/simplex/simplex_aux.h +7 -0
- simplinho-0.1.0/include/simplex/simplex_dual.h +619 -0
- simplinho-0.1.0/include/simplex/simplex_lu.h +747 -0
- simplinho-0.1.0/include/simplex/simplex_primal.h +426 -0
- simplinho-0.1.0/include/simplex/simplex_types.h +128 -0
- simplinho-0.1.0/publish.sh +40 -0
- simplinho-0.1.0/pyproject.toml +32 -0
- simplinho-0.1.0/src/README.md +4 -0
- simplinho-0.1.0/tests/simplex_modeling_api_demo.ipynb +222 -0
- simplinho-0.1.0/tests/simplex_vs_pulp_highs.ipynb +596 -0
- simplinho-0.1.0/tests/testing.py +1038 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.16)
|
|
2
|
+
|
|
3
|
+
project(simplinho VERSION 1.0 LANGUAGES CXX)
|
|
4
|
+
|
|
5
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
6
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
7
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
8
|
+
|
|
9
|
+
include(FetchContent)
|
|
10
|
+
|
|
11
|
+
set(PYBIND11_FINDPYTHON ON CACHE BOOL "Use CMake's FindPython for pybind11" FORCE)
|
|
12
|
+
set(PYBIND11_PYTHON_VERSION 3.13 CACHE STRING
|
|
13
|
+
"Python version to use when building pybind11 modules" FORCE)
|
|
14
|
+
find_package(Python 3.13 EXACT REQUIRED COMPONENTS Interpreter Development)
|
|
15
|
+
|
|
16
|
+
set(SIMPLEX_LOCAL_DEPS_ROOT "")
|
|
17
|
+
foreach(SIMPLEX_DEPS_CANDIDATE
|
|
18
|
+
"${CMAKE_CURRENT_LIST_DIR}/build-local/_deps"
|
|
19
|
+
"${CMAKE_CURRENT_LIST_DIR}/build/_deps"
|
|
20
|
+
"${CMAKE_CURRENT_LIST_DIR}/../build/_deps"
|
|
21
|
+
"${CMAKE_CURRENT_LIST_DIR}/../build-local/_deps")
|
|
22
|
+
if(EXISTS "${SIMPLEX_DEPS_CANDIDATE}")
|
|
23
|
+
set(SIMPLEX_LOCAL_DEPS_ROOT "${SIMPLEX_DEPS_CANDIDATE}")
|
|
24
|
+
break()
|
|
25
|
+
endif()
|
|
26
|
+
endforeach()
|
|
27
|
+
|
|
28
|
+
set(SIMPLEX_LOCAL_EIGEN_DIR
|
|
29
|
+
"${SIMPLEX_LOCAL_DEPS_ROOT}/eigen-src")
|
|
30
|
+
set(SIMPLEX_LOCAL_PYBIND11_DIR
|
|
31
|
+
"${SIMPLEX_LOCAL_DEPS_ROOT}/pybind11-src")
|
|
32
|
+
|
|
33
|
+
if(NOT TARGET Eigen3::Eigen)
|
|
34
|
+
if(EXISTS "${SIMPLEX_LOCAL_EIGEN_DIR}/Eigen/Core")
|
|
35
|
+
add_library(simplex_eigen INTERFACE)
|
|
36
|
+
target_include_directories(simplex_eigen INTERFACE
|
|
37
|
+
"${SIMPLEX_LOCAL_EIGEN_DIR}")
|
|
38
|
+
add_library(Eigen3::Eigen ALIAS simplex_eigen)
|
|
39
|
+
else()
|
|
40
|
+
FetchContent_Declare(
|
|
41
|
+
Eigen
|
|
42
|
+
GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
|
|
43
|
+
GIT_TAG 3.4.1
|
|
44
|
+
)
|
|
45
|
+
FetchContent_MakeAvailable(Eigen)
|
|
46
|
+
add_library(simplex_eigen INTERFACE)
|
|
47
|
+
target_include_directories(simplex_eigen INTERFACE
|
|
48
|
+
"${eigen_SOURCE_DIR}")
|
|
49
|
+
add_library(Eigen3::Eigen ALIAS simplex_eigen)
|
|
50
|
+
endif()
|
|
51
|
+
endif()
|
|
52
|
+
|
|
53
|
+
if(NOT TARGET pybind11::module)
|
|
54
|
+
if(EXISTS "${SIMPLEX_LOCAL_PYBIND11_DIR}/CMakeLists.txt")
|
|
55
|
+
add_subdirectory(
|
|
56
|
+
"${SIMPLEX_LOCAL_PYBIND11_DIR}"
|
|
57
|
+
"${CMAKE_CURRENT_BINARY_DIR}/pybind11-build"
|
|
58
|
+
)
|
|
59
|
+
else()
|
|
60
|
+
FetchContent_Declare(
|
|
61
|
+
pybind11
|
|
62
|
+
GIT_REPOSITORY https://github.com/pybind/pybind11.git
|
|
63
|
+
GIT_TAG v2.12.0
|
|
64
|
+
)
|
|
65
|
+
FetchContent_MakeAvailable(pybind11)
|
|
66
|
+
endif()
|
|
67
|
+
endif()
|
|
68
|
+
|
|
69
|
+
add_library(simplex_core INTERFACE)
|
|
70
|
+
target_include_directories(simplex_core INTERFACE
|
|
71
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/include")
|
|
72
|
+
target_link_libraries(simplex_core INTERFACE Eigen3::Eigen)
|
|
73
|
+
|
|
74
|
+
pybind11_add_module(simplinho bindings/simplex_bindings.cpp)
|
|
75
|
+
target_link_libraries(simplinho PRIVATE simplex_core pybind11::module)
|
|
76
|
+
|
|
77
|
+
install(TARGETS simplinho DESTINATION .)
|
simplinho-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laio O. Seman. All rights reserved.
|
|
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.
|
simplinho-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: simplinho
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple revised simplex LP solver with Python bindings
|
|
5
|
+
Keywords: linear programming,simplex,optimization,operations research
|
|
6
|
+
Author-Email: "Laio O. Seman" <laio@ufsc.br>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Laio O. Seman. All rights reserved.
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Classifier: Development Status :: 4 - Beta
|
|
30
|
+
Classifier: Intended Audience :: Science/Research
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
37
|
+
Classifier: Programming Language :: C++
|
|
38
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
39
|
+
Project-URL: Repository, https://github.com/lseman/simplinho
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<img src="assets/simplinho-logo.svg" alt="simplinho logo" width="720">
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
# simplinho
|
|
48
|
+
|
|
49
|
+
`simplinho` is a standalone revised simplex LP solver with a Python extension module and a small modeling API.
|
|
50
|
+
|
|
51
|
+
The core solver is header-only C++ in `include/simplex/`, the Python bindings live in `bindings/`, and the top-level build produces a `simplinho` module via `pybind11`.
|
|
52
|
+
|
|
53
|
+
## Highlights
|
|
54
|
+
|
|
55
|
+
- Primal revised simplex and dual revised simplex in one solver
|
|
56
|
+
- Automatic mode selection with `Auto`, `Primal`, and `Dual` modes
|
|
57
|
+
- Direct Phase II attempts with Phase I fallback when a feasible basis is not available
|
|
58
|
+
- Explicit handling of lower and upper bounds, including automatic reformulation of nonstandard variable bounds
|
|
59
|
+
- Dual bound flipping with Beale-style bound-flip ratio logic
|
|
60
|
+
- Presolve passes for row reduction, scaling, singleton elimination, bound tightening, dual fixing, and early infeasible/unbounded detection
|
|
61
|
+
- Markowitz LU factorization with rook pivoting and iterative refinement
|
|
62
|
+
- Forrest-Tomlin basis updates, with eta-stack updates as an alternative
|
|
63
|
+
- Multi-attempt crash basis construction with Markowitz-threshold triangularization,
|
|
64
|
+
rotating `hybrid`/`sprint`/`crash_ii`/`crash_iii` heuristics, and
|
|
65
|
+
presolved warm-start repair
|
|
66
|
+
- Adaptive pricing, Devex pricing, and most-negative pricing in both primal
|
|
67
|
+
and dual simplex
|
|
68
|
+
- Degeneracy management, anti-cycling support, and Harris-style ratio tests
|
|
69
|
+
- Rich solve outputs: basis data, reduced costs, dual values, shadow prices, internal tableau data, traces, and Farkas certificates
|
|
70
|
+
- A higher-level Python modeling layer with algebraic expressions, named variables, named constraints, and post-solve dual access
|
|
71
|
+
|
|
72
|
+
## What The Project Exposes
|
|
73
|
+
|
|
74
|
+
There are two main ways to use the solver:
|
|
75
|
+
|
|
76
|
+
1. `simplinho.RevisedSimplex`
|
|
77
|
+
Use the low-level matrix API directly:
|
|
78
|
+
`solve(A, b, c, l, u)` for `min c^T x` subject to `Ax = b` and `l <= x <= u`.
|
|
79
|
+
|
|
80
|
+
2. `simplinho.Model`
|
|
81
|
+
Use the modeling API with variables, expressions, constraints, and `minimize(...)` / `maximize(...)`.
|
|
82
|
+
|
|
83
|
+
The Python module also exposes:
|
|
84
|
+
|
|
85
|
+
- `RevisedSimplexOptions`
|
|
86
|
+
- `SimplexMode`
|
|
87
|
+
- `LPStatus`
|
|
88
|
+
- `LPBasis`
|
|
89
|
+
- `LPBasisStatus`
|
|
90
|
+
- `status_to_string(...)`
|
|
91
|
+
|
|
92
|
+
## Solver Features
|
|
93
|
+
|
|
94
|
+
### Algorithms
|
|
95
|
+
|
|
96
|
+
- Revised simplex implementation with both primal and dual pivoting
|
|
97
|
+
- Automatic fallback behavior when a direct solve needs Phase I work
|
|
98
|
+
- Support for bounded variables, free variables, shifted variables, and internally added slacks
|
|
99
|
+
- Bound-flip logic for dual iterations when enabled through `dual_allow_bound_flip`
|
|
100
|
+
|
|
101
|
+
### Numerics
|
|
102
|
+
|
|
103
|
+
- Dense Markowitz LU factorization
|
|
104
|
+
- Rook pivoting for more robust pivot selection
|
|
105
|
+
- Iterative refinement in both forward and transpose solves
|
|
106
|
+
- Configurable pivot tolerances, refactor frequency, and compression thresholds
|
|
107
|
+
- Forrest-Tomlin updates by default, or eta-stack updates via `basis_update = "eta"`
|
|
108
|
+
|
|
109
|
+
### Pricing And Stability
|
|
110
|
+
|
|
111
|
+
- `pricing_rule = "adaptive"` for steepest-edge on smaller dual bases and
|
|
112
|
+
Devex on larger ones, with periodic weight resets
|
|
113
|
+
- `pricing_rule = "devex"` for Devex pricing
|
|
114
|
+
- `pricing_rule = "most_negative"` for a simple reduced-cost rule in primal and
|
|
115
|
+
most-infeasible-row pricing in dual
|
|
116
|
+
- `crash_attempts`, `crash_markowitz_tol`, `crash_strategy`, and
|
|
117
|
+
`repair_mapped_basis` tune the initial basis search and post-presolve basis
|
|
118
|
+
repair
|
|
119
|
+
- Degeneracy tracking and anti-cycling support
|
|
120
|
+
- Harris-style primal and dual ratio tests
|
|
121
|
+
- Optional Bland rule toggle
|
|
122
|
+
|
|
123
|
+
### Presolve And Diagnostics
|
|
124
|
+
|
|
125
|
+
- Presolve with row/column simplifications, scaling, singleton eliminations, and bound tightening
|
|
126
|
+
- Early `infeasible` and `unbounded` detection in presolve when possible
|
|
127
|
+
- Verbose tracing with optional basis and presolve details
|
|
128
|
+
- Final internal tableau, reduced costs, dual values, and shadow prices on the solved internal model
|
|
129
|
+
- Farkas certificate output when infeasibility is proven through the dual path
|
|
130
|
+
|
|
131
|
+
## Build
|
|
132
|
+
|
|
133
|
+
The CMake project currently builds a Python extension named `simplinho`.
|
|
134
|
+
|
|
135
|
+
### Requirements
|
|
136
|
+
|
|
137
|
+
- CMake 3.16+
|
|
138
|
+
- A C++20 compiler
|
|
139
|
+
- Python 3.13 development headers
|
|
140
|
+
|
|
141
|
+
If local copies of Eigen or `pybind11` are not present in a nearby `_deps` directory, CMake will fetch them with `FetchContent`.
|
|
142
|
+
|
|
143
|
+
### Build Commands
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
cmake -S . -B build-local
|
|
147
|
+
cmake --build build-local -j
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
That produces a shared object like `build-local/simplinho.cpython-313-...so`.
|
|
151
|
+
|
|
152
|
+
## Low-Level Example
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
import sys
|
|
156
|
+
from pathlib import Path
|
|
157
|
+
|
|
158
|
+
import numpy as np
|
|
159
|
+
|
|
160
|
+
sys.path.insert(0, str(Path("build-local").resolve()))
|
|
161
|
+
|
|
162
|
+
import simplinho as simplex
|
|
163
|
+
|
|
164
|
+
A = np.array([
|
|
165
|
+
[1.0, 1.0],
|
|
166
|
+
])
|
|
167
|
+
b = np.array([4.0])
|
|
168
|
+
c = np.array([1.0, 2.0])
|
|
169
|
+
l = np.array([0.0, 0.0])
|
|
170
|
+
u = np.array([np.inf, np.inf])
|
|
171
|
+
|
|
172
|
+
options = simplex.RevisedSimplexOptions()
|
|
173
|
+
options.mode = simplex.SimplexMode.Auto
|
|
174
|
+
options.pricing_rule = "adaptive"
|
|
175
|
+
|
|
176
|
+
solver = simplex.RevisedSimplex(options)
|
|
177
|
+
solution = solver.solve(A, b, c, l, u)
|
|
178
|
+
|
|
179
|
+
print(simplex.status_to_string(solution.status))
|
|
180
|
+
print("objective:", solution.obj)
|
|
181
|
+
print("x:", solution.x)
|
|
182
|
+
print("iterations:", solution.iters)
|
|
183
|
+
print("stats:", solution.stats.as_dict())
|
|
184
|
+
print("log:", solution.log)
|
|
185
|
+
print("basis:", solution.basis_state)
|
|
186
|
+
print("dual values:", solution.dual_values_internal)
|
|
187
|
+
print("reduced costs:", solution.reduced_costs_internal)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
You can also reuse that basis as a warm start after bound changes:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
basis = solution.basis_state
|
|
194
|
+
|
|
195
|
+
u2 = np.array([1.5, np.inf])
|
|
196
|
+
warm = solver.solve(A, b, c, l, u2, basis=basis)
|
|
197
|
+
print(warm.stats.basis_start)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
If you keep the same `RevisedSimplex` instance in `Dual` mode, it will also
|
|
201
|
+
automatically reuse the last valid basis on later `solve(...)` calls with the
|
|
202
|
+
same row/column dimensions. That makes repeated bound-fixing re-solves behave
|
|
203
|
+
more like a branch-and-bound node LP loop:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
options.mode = simplex.SimplexMode.Dual
|
|
207
|
+
solver = simplex.RevisedSimplex(options)
|
|
208
|
+
|
|
209
|
+
root = solver.solve(A, b, c, l, u)
|
|
210
|
+
u_node = np.array([1.5, np.inf])
|
|
211
|
+
node = solver.solve(A, b, c, l, u_node)
|
|
212
|
+
print(node.stats.basis_start)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Modeling API Example
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
import sys
|
|
219
|
+
from pathlib import Path
|
|
220
|
+
|
|
221
|
+
sys.path.insert(0, str(Path("build-local").resolve()))
|
|
222
|
+
|
|
223
|
+
import simplinho as simplex
|
|
224
|
+
|
|
225
|
+
model = simplex.Model()
|
|
226
|
+
|
|
227
|
+
x = model.addVar("x", lb=0.0)
|
|
228
|
+
y = model.addVar("y", lb=0.0)
|
|
229
|
+
|
|
230
|
+
c1 = model.addConstr(x <= 3, name="cap_x")
|
|
231
|
+
c2 = model.addConstr(y <= 2, name="cap_y")
|
|
232
|
+
c3 = model.addConstr(x + 2 * y <= 6, name="mix_cap")
|
|
233
|
+
|
|
234
|
+
model.maximize(x + y)
|
|
235
|
+
solution = model.solve()
|
|
236
|
+
|
|
237
|
+
print("status :", simplex.status_to_string(solution.status))
|
|
238
|
+
print("objective:", solution.obj)
|
|
239
|
+
print("x :", solution.value(x))
|
|
240
|
+
print("y :", solution.value("y"))
|
|
241
|
+
print("all vars :", solution.values)
|
|
242
|
+
print("stats :", solution.stats.as_dict())
|
|
243
|
+
print("basis :", solution.basis)
|
|
244
|
+
print("log :", solution.log)
|
|
245
|
+
print("dual c1 :", c1.pi)
|
|
246
|
+
print("dual c2 :", c2.pi)
|
|
247
|
+
print("dual c3 :", c3.pi)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The modeling layer also supports live edits after construction:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
x.obj = 3.0
|
|
254
|
+
c3.rhs = 5.0
|
|
255
|
+
c3.set_coeff(y, 2.0)
|
|
256
|
+
|
|
257
|
+
solution = model.reoptimize()
|
|
258
|
+
|
|
259
|
+
model.deleteConstr(c1)
|
|
260
|
+
model.deleteVar(x)
|
|
261
|
+
solution = model.reoptimize()
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
When the model structure stays the same, `reoptimize()` will automatically try
|
|
265
|
+
to reuse the last valid basis after edits like bound changes, RHS updates,
|
|
266
|
+
coefficient changes, or objective changes. You can also pass an explicit saved
|
|
267
|
+
basis for fast dual re-solves:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
basis = solution.basis
|
|
271
|
+
x.ub = 1.5
|
|
272
|
+
model.options.mode = simplex.SimplexMode.Dual
|
|
273
|
+
solution = model.reoptimize(basis)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Useful Outputs
|
|
277
|
+
|
|
278
|
+
The low-level `LPSolution` object includes more than just the primal vector:
|
|
279
|
+
|
|
280
|
+
- `status`, `obj`, `x`, `iters`
|
|
281
|
+
- `stats` with typed solve telemetry and `as_dict()`
|
|
282
|
+
- `basis_state` / `basis` for reusable warm starts
|
|
283
|
+
- `log_lines` and `log` for verbose solver traces
|
|
284
|
+
- `basis`, `basis_internal`, `nonbasis_internal`
|
|
285
|
+
- `tableau`, `tableau_rhs`, `has_internal_tableau`
|
|
286
|
+
- `reduced_costs_internal`
|
|
287
|
+
- `dual_values_internal`
|
|
288
|
+
- `shadow_prices_internal`
|
|
289
|
+
- `trace`
|
|
290
|
+
- `info`
|
|
291
|
+
- `farkas_y`, `farkas_y_internal`, `farkas_has_cert`
|
|
292
|
+
- `primal_ray`, `primal_ray_internal`, `primal_ray_has_cert`
|
|
293
|
+
|
|
294
|
+
The modeling layer wraps that in `ModelSolution`, while still exposing the raw solve result as `solution.raw`.
|
|
295
|
+
|
|
296
|
+
## Repository Layout
|
|
297
|
+
|
|
298
|
+
- `include/simplex/`: header-only solver core, presolve, LU, pricing, and degeneracy helpers
|
|
299
|
+
- `bindings/`: `pybind11` bindings and modeling API
|
|
300
|
+
- `src/`: reserved for future non-header sources
|
|
301
|
+
- `simplex_modeling_api_demo.ipynb`: notebook showing the modeling API in action
|
|
302
|
+
- `CMakeLists.txt`: standalone build for the Python module
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/simplinho-logo.svg" alt="simplinho logo" width="720">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# simplinho
|
|
6
|
+
|
|
7
|
+
`simplinho` is a standalone revised simplex LP solver with a Python extension module and a small modeling API.
|
|
8
|
+
|
|
9
|
+
The core solver is header-only C++ in `include/simplex/`, the Python bindings live in `bindings/`, and the top-level build produces a `simplinho` module via `pybind11`.
|
|
10
|
+
|
|
11
|
+
## Highlights
|
|
12
|
+
|
|
13
|
+
- Primal revised simplex and dual revised simplex in one solver
|
|
14
|
+
- Automatic mode selection with `Auto`, `Primal`, and `Dual` modes
|
|
15
|
+
- Direct Phase II attempts with Phase I fallback when a feasible basis is not available
|
|
16
|
+
- Explicit handling of lower and upper bounds, including automatic reformulation of nonstandard variable bounds
|
|
17
|
+
- Dual bound flipping with Beale-style bound-flip ratio logic
|
|
18
|
+
- Presolve passes for row reduction, scaling, singleton elimination, bound tightening, dual fixing, and early infeasible/unbounded detection
|
|
19
|
+
- Markowitz LU factorization with rook pivoting and iterative refinement
|
|
20
|
+
- Forrest-Tomlin basis updates, with eta-stack updates as an alternative
|
|
21
|
+
- Multi-attempt crash basis construction with Markowitz-threshold triangularization,
|
|
22
|
+
rotating `hybrid`/`sprint`/`crash_ii`/`crash_iii` heuristics, and
|
|
23
|
+
presolved warm-start repair
|
|
24
|
+
- Adaptive pricing, Devex pricing, and most-negative pricing in both primal
|
|
25
|
+
and dual simplex
|
|
26
|
+
- Degeneracy management, anti-cycling support, and Harris-style ratio tests
|
|
27
|
+
- Rich solve outputs: basis data, reduced costs, dual values, shadow prices, internal tableau data, traces, and Farkas certificates
|
|
28
|
+
- A higher-level Python modeling layer with algebraic expressions, named variables, named constraints, and post-solve dual access
|
|
29
|
+
|
|
30
|
+
## What The Project Exposes
|
|
31
|
+
|
|
32
|
+
There are two main ways to use the solver:
|
|
33
|
+
|
|
34
|
+
1. `simplinho.RevisedSimplex`
|
|
35
|
+
Use the low-level matrix API directly:
|
|
36
|
+
`solve(A, b, c, l, u)` for `min c^T x` subject to `Ax = b` and `l <= x <= u`.
|
|
37
|
+
|
|
38
|
+
2. `simplinho.Model`
|
|
39
|
+
Use the modeling API with variables, expressions, constraints, and `minimize(...)` / `maximize(...)`.
|
|
40
|
+
|
|
41
|
+
The Python module also exposes:
|
|
42
|
+
|
|
43
|
+
- `RevisedSimplexOptions`
|
|
44
|
+
- `SimplexMode`
|
|
45
|
+
- `LPStatus`
|
|
46
|
+
- `LPBasis`
|
|
47
|
+
- `LPBasisStatus`
|
|
48
|
+
- `status_to_string(...)`
|
|
49
|
+
|
|
50
|
+
## Solver Features
|
|
51
|
+
|
|
52
|
+
### Algorithms
|
|
53
|
+
|
|
54
|
+
- Revised simplex implementation with both primal and dual pivoting
|
|
55
|
+
- Automatic fallback behavior when a direct solve needs Phase I work
|
|
56
|
+
- Support for bounded variables, free variables, shifted variables, and internally added slacks
|
|
57
|
+
- Bound-flip logic for dual iterations when enabled through `dual_allow_bound_flip`
|
|
58
|
+
|
|
59
|
+
### Numerics
|
|
60
|
+
|
|
61
|
+
- Dense Markowitz LU factorization
|
|
62
|
+
- Rook pivoting for more robust pivot selection
|
|
63
|
+
- Iterative refinement in both forward and transpose solves
|
|
64
|
+
- Configurable pivot tolerances, refactor frequency, and compression thresholds
|
|
65
|
+
- Forrest-Tomlin updates by default, or eta-stack updates via `basis_update = "eta"`
|
|
66
|
+
|
|
67
|
+
### Pricing And Stability
|
|
68
|
+
|
|
69
|
+
- `pricing_rule = "adaptive"` for steepest-edge on smaller dual bases and
|
|
70
|
+
Devex on larger ones, with periodic weight resets
|
|
71
|
+
- `pricing_rule = "devex"` for Devex pricing
|
|
72
|
+
- `pricing_rule = "most_negative"` for a simple reduced-cost rule in primal and
|
|
73
|
+
most-infeasible-row pricing in dual
|
|
74
|
+
- `crash_attempts`, `crash_markowitz_tol`, `crash_strategy`, and
|
|
75
|
+
`repair_mapped_basis` tune the initial basis search and post-presolve basis
|
|
76
|
+
repair
|
|
77
|
+
- Degeneracy tracking and anti-cycling support
|
|
78
|
+
- Harris-style primal and dual ratio tests
|
|
79
|
+
- Optional Bland rule toggle
|
|
80
|
+
|
|
81
|
+
### Presolve And Diagnostics
|
|
82
|
+
|
|
83
|
+
- Presolve with row/column simplifications, scaling, singleton eliminations, and bound tightening
|
|
84
|
+
- Early `infeasible` and `unbounded` detection in presolve when possible
|
|
85
|
+
- Verbose tracing with optional basis and presolve details
|
|
86
|
+
- Final internal tableau, reduced costs, dual values, and shadow prices on the solved internal model
|
|
87
|
+
- Farkas certificate output when infeasibility is proven through the dual path
|
|
88
|
+
|
|
89
|
+
## Build
|
|
90
|
+
|
|
91
|
+
The CMake project currently builds a Python extension named `simplinho`.
|
|
92
|
+
|
|
93
|
+
### Requirements
|
|
94
|
+
|
|
95
|
+
- CMake 3.16+
|
|
96
|
+
- A C++20 compiler
|
|
97
|
+
- Python 3.13 development headers
|
|
98
|
+
|
|
99
|
+
If local copies of Eigen or `pybind11` are not present in a nearby `_deps` directory, CMake will fetch them with `FetchContent`.
|
|
100
|
+
|
|
101
|
+
### Build Commands
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cmake -S . -B build-local
|
|
105
|
+
cmake --build build-local -j
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That produces a shared object like `build-local/simplinho.cpython-313-...so`.
|
|
109
|
+
|
|
110
|
+
## Low-Level Example
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import sys
|
|
114
|
+
from pathlib import Path
|
|
115
|
+
|
|
116
|
+
import numpy as np
|
|
117
|
+
|
|
118
|
+
sys.path.insert(0, str(Path("build-local").resolve()))
|
|
119
|
+
|
|
120
|
+
import simplinho as simplex
|
|
121
|
+
|
|
122
|
+
A = np.array([
|
|
123
|
+
[1.0, 1.0],
|
|
124
|
+
])
|
|
125
|
+
b = np.array([4.0])
|
|
126
|
+
c = np.array([1.0, 2.0])
|
|
127
|
+
l = np.array([0.0, 0.0])
|
|
128
|
+
u = np.array([np.inf, np.inf])
|
|
129
|
+
|
|
130
|
+
options = simplex.RevisedSimplexOptions()
|
|
131
|
+
options.mode = simplex.SimplexMode.Auto
|
|
132
|
+
options.pricing_rule = "adaptive"
|
|
133
|
+
|
|
134
|
+
solver = simplex.RevisedSimplex(options)
|
|
135
|
+
solution = solver.solve(A, b, c, l, u)
|
|
136
|
+
|
|
137
|
+
print(simplex.status_to_string(solution.status))
|
|
138
|
+
print("objective:", solution.obj)
|
|
139
|
+
print("x:", solution.x)
|
|
140
|
+
print("iterations:", solution.iters)
|
|
141
|
+
print("stats:", solution.stats.as_dict())
|
|
142
|
+
print("log:", solution.log)
|
|
143
|
+
print("basis:", solution.basis_state)
|
|
144
|
+
print("dual values:", solution.dual_values_internal)
|
|
145
|
+
print("reduced costs:", solution.reduced_costs_internal)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
You can also reuse that basis as a warm start after bound changes:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
basis = solution.basis_state
|
|
152
|
+
|
|
153
|
+
u2 = np.array([1.5, np.inf])
|
|
154
|
+
warm = solver.solve(A, b, c, l, u2, basis=basis)
|
|
155
|
+
print(warm.stats.basis_start)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
If you keep the same `RevisedSimplex` instance in `Dual` mode, it will also
|
|
159
|
+
automatically reuse the last valid basis on later `solve(...)` calls with the
|
|
160
|
+
same row/column dimensions. That makes repeated bound-fixing re-solves behave
|
|
161
|
+
more like a branch-and-bound node LP loop:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
options.mode = simplex.SimplexMode.Dual
|
|
165
|
+
solver = simplex.RevisedSimplex(options)
|
|
166
|
+
|
|
167
|
+
root = solver.solve(A, b, c, l, u)
|
|
168
|
+
u_node = np.array([1.5, np.inf])
|
|
169
|
+
node = solver.solve(A, b, c, l, u_node)
|
|
170
|
+
print(node.stats.basis_start)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Modeling API Example
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
import sys
|
|
177
|
+
from pathlib import Path
|
|
178
|
+
|
|
179
|
+
sys.path.insert(0, str(Path("build-local").resolve()))
|
|
180
|
+
|
|
181
|
+
import simplinho as simplex
|
|
182
|
+
|
|
183
|
+
model = simplex.Model()
|
|
184
|
+
|
|
185
|
+
x = model.addVar("x", lb=0.0)
|
|
186
|
+
y = model.addVar("y", lb=0.0)
|
|
187
|
+
|
|
188
|
+
c1 = model.addConstr(x <= 3, name="cap_x")
|
|
189
|
+
c2 = model.addConstr(y <= 2, name="cap_y")
|
|
190
|
+
c3 = model.addConstr(x + 2 * y <= 6, name="mix_cap")
|
|
191
|
+
|
|
192
|
+
model.maximize(x + y)
|
|
193
|
+
solution = model.solve()
|
|
194
|
+
|
|
195
|
+
print("status :", simplex.status_to_string(solution.status))
|
|
196
|
+
print("objective:", solution.obj)
|
|
197
|
+
print("x :", solution.value(x))
|
|
198
|
+
print("y :", solution.value("y"))
|
|
199
|
+
print("all vars :", solution.values)
|
|
200
|
+
print("stats :", solution.stats.as_dict())
|
|
201
|
+
print("basis :", solution.basis)
|
|
202
|
+
print("log :", solution.log)
|
|
203
|
+
print("dual c1 :", c1.pi)
|
|
204
|
+
print("dual c2 :", c2.pi)
|
|
205
|
+
print("dual c3 :", c3.pi)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The modeling layer also supports live edits after construction:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
x.obj = 3.0
|
|
212
|
+
c3.rhs = 5.0
|
|
213
|
+
c3.set_coeff(y, 2.0)
|
|
214
|
+
|
|
215
|
+
solution = model.reoptimize()
|
|
216
|
+
|
|
217
|
+
model.deleteConstr(c1)
|
|
218
|
+
model.deleteVar(x)
|
|
219
|
+
solution = model.reoptimize()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
When the model structure stays the same, `reoptimize()` will automatically try
|
|
223
|
+
to reuse the last valid basis after edits like bound changes, RHS updates,
|
|
224
|
+
coefficient changes, or objective changes. You can also pass an explicit saved
|
|
225
|
+
basis for fast dual re-solves:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
basis = solution.basis
|
|
229
|
+
x.ub = 1.5
|
|
230
|
+
model.options.mode = simplex.SimplexMode.Dual
|
|
231
|
+
solution = model.reoptimize(basis)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Useful Outputs
|
|
235
|
+
|
|
236
|
+
The low-level `LPSolution` object includes more than just the primal vector:
|
|
237
|
+
|
|
238
|
+
- `status`, `obj`, `x`, `iters`
|
|
239
|
+
- `stats` with typed solve telemetry and `as_dict()`
|
|
240
|
+
- `basis_state` / `basis` for reusable warm starts
|
|
241
|
+
- `log_lines` and `log` for verbose solver traces
|
|
242
|
+
- `basis`, `basis_internal`, `nonbasis_internal`
|
|
243
|
+
- `tableau`, `tableau_rhs`, `has_internal_tableau`
|
|
244
|
+
- `reduced_costs_internal`
|
|
245
|
+
- `dual_values_internal`
|
|
246
|
+
- `shadow_prices_internal`
|
|
247
|
+
- `trace`
|
|
248
|
+
- `info`
|
|
249
|
+
- `farkas_y`, `farkas_y_internal`, `farkas_has_cert`
|
|
250
|
+
- `primal_ray`, `primal_ray_internal`, `primal_ray_has_cert`
|
|
251
|
+
|
|
252
|
+
The modeling layer wraps that in `ModelSolution`, while still exposing the raw solve result as `solution.raw`.
|
|
253
|
+
|
|
254
|
+
## Repository Layout
|
|
255
|
+
|
|
256
|
+
- `include/simplex/`: header-only solver core, presolve, LU, pricing, and degeneracy helpers
|
|
257
|
+
- `bindings/`: `pybind11` bindings and modeling API
|
|
258
|
+
- `src/`: reserved for future non-header sources
|
|
259
|
+
- `simplex_modeling_api_demo.ipynb`: notebook showing the modeling API in action
|
|
260
|
+
- `CMakeLists.txt`: standalone build for the Python module
|