Skip to content

Commit 64610aa

Browse files
h-g-sCopilot
andauthored
Migrate from cbcbox to mipster for the bundled CBC library (#424)
* Migrate from cbcbox to mipster for the bundled CBC library cbcbox shipped CBC source builds; mipster (https://pypi.org/project/mipster/) ships self-contained, hardware-tuned wheels for Linux x86_64/aarch64, macOS x86_64/arm64, and Windows x86_64 with CPUID auto-dispatch (generic / AVX2 / Haswell / NEON variants). - Replace `cbcbox>=2.929` with `mipster>=0.2.0`. - Use `mipster.lib_path()` instead of platform-specific path joining; this returns the right shared library for the current OS and selected variant. - Drop the manual libCbc.so / libCbc-0.dll / libCbc.dylib branching. - Keep `os.add_dll_directory()` on Windows so the loader can find the sibling MinGW runtime DLLs that mipster bundles next to libmipster-N.dll. - Remove unused Cbc_get/setAllowablePercentageGap CFFI declarations (these are not exported by libmipster and were never called). * chore: require mipster>=0.2.4 Version 0.2.4 fixes three C API bugs required for correctness: - Auto-generate column names so translate() works in lazy callbacks - Sync bound==value when proven optimal (prevents spurious assertion failures) - Restore rSlk/rActv pointers after MIP solve (prevents null-pointer crash) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style: apply black formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style: apply black 22.3.0 formatting (matching pre-commit config) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: gracefully disable HiGHS on Windows when C API is not accessible highspy's _core.pyd on Windows only exports the Python init symbol (PyInit__core) -- the C API functions like Highs_create are compiled in but not exported as DLL symbols. On Linux/macOS all symbols in a shared library are visible, so ffi.dlopen() works there. After loading the library via ffi.dlopen(), test whether Highs_create is actually accessible. If not (Windows + highspy), set has_highs=False so that all HiGHS tests are skipped gracefully instead of crashing with 'function/symbol not found' errors. Users on Windows who need HiGHS can install highsbox (which ships a proper highs.dll with exported C symbols) and set PMIP_HIGHS_LIBRARY to point to it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use highsbox as fallback for HiGHS C API on Windows highspy's _core.pyd on Windows does not export C symbols (only PyInit_* is exported from a Windows DLL by default). On Linux/macOS the .so exports all symbols so ffi.dlopen + Highs_create works fine; on Windows it raises AttributeError. Changes: - After dlopen succeeds, probe Highs_create accessibility. If it fails (Windows + highspy), automatically retry with highsbox's highs.dll. - highsbox is added as a Windows-only optional dependency in [highs]: highsbox>=1.9.0; sys_platform == 'win32' so 'pip install mip[highs]' on Windows installs both highspy (Python bindings) and highsbox (C API DLL), giving full HiGHS functionality. - Hoist the highsbox path resolver to a module-level helper function _get_highsbox_libfile() so it is reachable both at initial load and at the fallback probe point. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: cancel in-progress runs when new commit is pushed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: guard __del__ against partially-constructed objects (PyPy safe) On PyPy, the GC may call __del__ on an object whose __init__ raised before all attributes were set (e.g. SolverGurobi.__init__ raises early when Gurobi is not installed, before _ownsModel/_venv_loaded are set). CPython avoids this because reference-counting drops the object immediately; PyPy's tracing GC processes it later. Replace bare attribute access in __del__ with getattr(..., default) in: - SolverGurobi.__del__: guard _ownsModel and _venv_loaded - SolverHighs.__del__: guard _model (avoid calling Highs_destroy(NULL)) - SolverCbc.__del__: guard _model (avoid calling Cbc_deleteModel(NULL)) - SolverOsi.__del__: guard owns_solver Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3f03388 commit 64610aa

5 files changed

Lines changed: 163 additions & 121 deletions

File tree

.github/workflows/github-ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ name: CI
22

33
on: [push, pull_request]
44

5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
59
jobs:
610

711
pre-commit:

mip/cbc.py

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -70,36 +70,20 @@
7070
cbclib = ffi.dlopen(libfile)
7171
os.chdir(old_dir)
7272
else:
73-
import cbcbox as _cbcbox
74-
75-
_lib_dir = _cbcbox.cbc_lib_dir()
76-
if "linux" in platform.lower():
77-
if not os_is_64_bit:
78-
raise NotImplementedError("Linux 32 bits platform not supported.")
79-
libfile = os.path.join(_lib_dir, "libCbc.so")
80-
elif platform.lower().startswith("win"):
81-
if not os_is_64_bit:
82-
raise NotImplementedError("Win32 platform not supported.")
83-
# autotools/MinGW places DLLs under bin/, not lib/
84-
_bin_dir = os.path.join(_cbcbox.cbc_dist_dir(), "bin")
85-
libfile = os.path.join(_bin_dir, "libCbc-0.dll")
86-
if not os.path.exists(libfile):
87-
raise FileNotFoundError(
88-
"libCbc-0.dll not found in cbcbox Windows distribution at"
89-
" {}. The cbcbox Windows wheel may only contain a static"
90-
" libCbc.a. A shared libCbc-0.dll is required.".format(_bin_dir)
91-
)
92-
# Python 3.8+ ignores PATH for DLL resolution; use add_dll_directory
73+
import mipster as _mipster
74+
75+
if not os_is_64_bit:
76+
raise NotImplementedError("32-bit platforms are not supported.")
77+
libfile = _mipster.lib_path()
78+
if platform.lower().startswith("win"):
79+
# Python 3.8+ ignores PATH for DLL resolution; let Windows find
80+
# any sibling DLLs (libgcc, libstdc++, libwinpthread) via
81+
# add_dll_directory on the directory holding libmipster-N.dll.
82+
_bin_dir = os.path.dirname(libfile)
9383
if hasattr(os, "add_dll_directory"):
9484
os.add_dll_directory(_bin_dir)
9585
elif _bin_dir not in os.environ.get("PATH", ""):
9686
os.environ["PATH"] = _bin_dir + ";" + os.environ["PATH"]
97-
elif platform.lower().startswith("darwin") or platform.lower().startswith(
98-
"macos"
99-
):
100-
libfile = os.path.join(_lib_dir, "libCbc.dylib")
101-
else:
102-
raise NotImplementedError("Your operating system/platform is not supported")
10387
cbclib = ffi.dlopen(libfile)
10488
has_cbc = True
10589
except Exception as e:
@@ -317,11 +301,6 @@
317301
void Cbc_setAllowableFractionGap(Cbc_Model *model,
318302
double allowedFracionGap);
319303
320-
double Cbc_getAllowablePercentageGap(Cbc_Model *model);
321-
322-
void Cbc_setAllowablePercentageGap(Cbc_Model *model,
323-
double allowedPercentageGap);
324-
325304
double Cbc_getMaximumSeconds(Cbc_Model *model);
326305
327306
void Cbc_setMaximumSeconds(Cbc_Model *model, double maxSeconds);
@@ -1022,7 +1001,9 @@ def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None):
10221001
strengthenPacking = cbclib.Cbc_strengthenPackingRows
10231002
strengthenPacking(self._model, nr, idxr)
10241003

1025-
def optimize(self, relax: bool = False, lp_preprocess: bool = False) -> OptimizationStatus:
1004+
def optimize(
1005+
self, relax: bool = False, lp_preprocess: bool = False
1006+
) -> OptimizationStatus:
10261007
# get name indexes from an osi problem
10271008
def cbc_get_osi_name_indexes(osi_solver) -> Dict[str, int]:
10281009
nameIdx = {}
@@ -1203,7 +1184,9 @@ def cbc_cut_callback(osi_solver, osi_cuts, app_data, depth, npass):
12031184

12041185
# user-specified cut passes override the cuts-level default
12051186
if self.model.cut_passes != -1:
1206-
cbclib.Cbc_setIntParam(self._model, INT_PARAM_CUT_PASS, self.model.cut_passes)
1187+
cbclib.Cbc_setIntParam(
1188+
self._model, INT_PARAM_CUT_PASS, self.model.cut_passes
1189+
)
12071190

12081191
if self.model.clique == 0:
12091192
cbc_set_parameter(self, "clique", "off")
@@ -1662,7 +1645,8 @@ def remove_vars(self, varsList: List[int]):
16621645
cbclib.Cbc_deleteCols(self._model, len(varsList), idx)
16631646

16641647
def __del__(self):
1665-
cbclib.Cbc_deleteModel(self._model)
1648+
if getattr(self, "_model", None) is not None:
1649+
cbclib.Cbc_deleteModel(self._model)
16661650

16671651
def get_problem_name(self) -> str:
16681652
namep = self.__name_space
@@ -1793,7 +1777,7 @@ def __clear_sol(self: "SolverOsi"):
17931777
self.__obj_val = None
17941778

17951779
def __del__(self):
1796-
if self.owns_solver:
1780+
if getattr(self, "owns_solver", False):
17971781
cbclib.Osi_deleteSolver(self.osi)
17981782

17991783
def add_var(

mip/gurobi.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,12 @@ def __clear_sol(self):
428428
self.__obj_val = None
429429

430430
def __del__(self):
431-
# freeing Gurobi model and environment
432-
if self._ownsModel:
431+
# Guard against partially-constructed objects (e.g. PyPy GC may call
432+
# __del__ even if __init__ raised before setting these attributes).
433+
if getattr(self, "_ownsModel", False):
433434
if self._model:
434435
GRBfreemodel(self._model)
435-
if self._env and self._venv_loaded:
436+
if self._env and getattr(self, "_venv_loaded", False):
436437
GRBfreeenv(self._env)
437438

438439
def add_var(
@@ -589,7 +590,9 @@ def set_max_iter(self, max_iter: int):
589590
def set_num_threads(self, threads: int):
590591
self.__threads = threads
591592

592-
def optimize(self, relax: bool = False, lp_preprocess: bool = False) -> OptimizationStatus:
593+
def optimize(
594+
self, relax: bool = False, lp_preprocess: bool = False
595+
) -> OptimizationStatus:
593596

594597
# todo add branch_selector and incumbent_updater callbacks
595598
@ffi.callback(
@@ -802,9 +805,7 @@ def callback(
802805
self.__obj_val = self.get_dbl_attr("ObjVal")
803806
self.__x = ffi.new("double[{}]".format(self.num_cols()))
804807
attr = "X".encode("utf-8")
805-
st = GRBgetdblattrarray(
806-
self._model, attr, 0, self.num_cols(), self.__x
807-
)
808+
st = GRBgetdblattrarray(self._model, attr, 0, self.num_cols(), self.__x)
808809
if st:
809810
raise ParameterNotAvailable("Error querying Gurobi solution")
810811
# duals are only valid at optimality for Gurobi, skip Pi/RC

0 commit comments

Comments
 (0)