Skip to content

Commit

Permalink
Merge branch 'coramin2' into mindtpy-presolve-coramin
Browse files Browse the repository at this point in the history
  • Loading branch information
ZedongPeng committed Apr 11, 2024
2 parents 3a872b0 + c1d0324 commit 54a0533
Show file tree
Hide file tree
Showing 92 changed files with 19,320 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ omit =
setup.py
*/tests/*
*/tmp/*
*/minlplib/*

# The [run] section must be at the end, as the build harness will add a
# "data_file" directive to the end of this file.
Expand All @@ -17,3 +18,4 @@ source =
omit =
# github actions creates a cache directory we don't want measured
cache/*
*/minlplib/*
45 changes: 44 additions & 1 deletion .github/workflows/test_branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defaults:
env:
PYTHONWARNINGS: ignore::UserWarning
PYTHON_CORE_PKGS: wheel
PYPI_ONLY: z3-solver
PYPI_ONLY: z3-solver pybnb
PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels
CACHE_VER: v221013.1
NEOS_EMAIL: [email protected]
Expand Down Expand Up @@ -94,6 +94,14 @@ jobs:
PYENV: conda
PACKAGES: mpi4py

- os: ubuntu-latest
python: 3.11
other: /singletest
category: "-m 'neos or importtest'"
skip_doctest: 1
TARGET: linux
PYENV: pip

- os: ubuntu-latest
python: '3.10'
other: /cython
Expand Down Expand Up @@ -149,6 +157,24 @@ jobs:
# path: cache/os
# key: pkg-${{env.CACHE_VER}}.0-${{runner.os}}

- name: install GiNaC
if: matrix.other == '/singletest'
run: |
cd ..
curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2
tar -xvf cln-1.3.7.tar.bz2
cd cln-1.3.7
./configure
make -j 2
sudo make install
cd ..
curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2
tar -xvf ginac-1.8.7.tar.bz2
cd ginac-1.8.7
./configure
make -j 2
sudo make install
- name: TPL package download cache
uses: actions/cache@v4
if: ${{ ! matrix.slim }}
Expand Down Expand Up @@ -268,6 +294,8 @@ jobs:
|| echo "WARNING: Gurobi is not available"
python -m pip install --cache-dir cache/pip xpress \
|| echo "WARNING: Xpress Community Edition is not available"
python -m pip install --cache-dir cache/pip pybnb \
|| echo "WARNING: pybnb is not available"
if [[ ${{matrix.python}} == pypy* ]]; then
echo "skipping wntr for pypy"
else
Expand Down Expand Up @@ -337,6 +365,8 @@ jobs:
echo "*** Install Pyomo dependencies ***"
# Note: this will fail the build if any installation fails (or
# possibly if it outputs messages to stderr)
CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES metis"
PYPI_DEPENDENCIES="$PYPI_DEPENDENCIES metis"
conda install --update-deps -y $CONDA_DEPENDENCIES
if test -z "${{matrix.slim}}"; then
PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g')
Expand Down Expand Up @@ -609,13 +639,26 @@ jobs:
echo ""
pyomo build-extensions --parallel 2
- name: Install GiNaC Interface
if: matrix.other == '/singletest'
run: |
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV
cd pyomo/contrib/simplification/
$PYTHON_EXE build.py --inplace
- name: Report pyomo plugin information
run: |
echo "$PATH"
pyomo help --solvers || exit 1
pyomo help --transformations || exit 1
pyomo help --writers || exit 1
- name: Run Simplification Tests
if: matrix.other == '/singletest'
run: |
pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml"
- name: Run Pyomo tests
if: matrix.mpi == 0
run: |
Expand Down
37 changes: 36 additions & 1 deletion .github/workflows/test_pr_and_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defaults:
env:
PYTHONWARNINGS: ignore::UserWarning
PYTHON_CORE_PKGS: wheel
PYPI_ONLY: z3-solver
PYPI_ONLY: z3-solver pybnb
PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels
CACHE_VER: v221013.1
NEOS_EMAIL: [email protected]
Expand Down Expand Up @@ -179,6 +179,24 @@ jobs:
# path: cache/os
# key: pkg-${{env.CACHE_VER}}.0-${{runner.os}}

- name: install GiNaC
if: matrix.other == '/singletest'
run: |
cd ..
curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2
tar -xvf cln-1.3.7.tar.bz2
cd cln-1.3.7
./configure
make -j 2
sudo make install
cd ..
curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2
tar -xvf ginac-1.8.7.tar.bz2
cd ginac-1.8.7
./configure
make -j 2
sudo make install
- name: TPL package download cache
uses: actions/cache@v4
if: ${{ ! matrix.slim }}
Expand Down Expand Up @@ -298,6 +316,8 @@ jobs:
|| echo "WARNING: Gurobi is not available"
python -m pip install --cache-dir cache/pip xpress \
|| echo "WARNING: Xpress Community Edition is not available"
python -m pip install --cache-dir cache/pip pybnb \
|| echo "WARNING: pybnb is not available"
if [[ ${{matrix.python}} == pypy* ]]; then
echo "skipping wntr for pypy"
else
Expand Down Expand Up @@ -366,6 +386,8 @@ jobs:
echo "*** Install Pyomo dependencies ***"
# Note: this will fail the build if any installation fails (or
# possibly if it outputs messages to stderr)
CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES metis"
PYPI_DEPENDENCIES="$PYPI_DEPENDENCIES metis"
conda install --update-deps -q -y $CONDA_DEPENDENCIES
if test -z "${{matrix.slim}}"; then
PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g')
Expand Down Expand Up @@ -638,13 +660,26 @@ jobs:
echo ""
pyomo build-extensions --parallel 2
- name: Install GiNaC Interface
if: matrix.other == '/singletest'
run: |
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV
cd pyomo/contrib/simplification/
$PYTHON_EXE build.py --inplace
- name: Report pyomo plugin information
run: |
echo "$PATH"
pyomo help --solvers || exit 1
pyomo help --transformations || exit 1
pyomo help --writers || exit 1
- name: Run Simplification Tests
if: matrix.other == '/singletest'
run: |
pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml"
- name: Run Pyomo tests
if: matrix.mpi == 0
run: |
Expand Down
2 changes: 1 addition & 1 deletion pyomo/contrib/appsi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1641,7 +1641,7 @@ def solve(

return legacy_results

def available(self, exception_flag=True):
def available(self, exception_flag=False):
ans = super(LegacySolverInterface, self).available()
if exception_flag and not ans:
raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).')
Expand Down
6 changes: 4 additions & 2 deletions pyomo/contrib/appsi/fbbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ def _add_params(self, params: List[_ParamData]):
cparams = cmodel.create_params(len(params))
for ndx, p in enumerate(params):
cp = cparams[ndx]
cp.value = p.value
if p.value is not None:
cp.value = p.value
self._param_map[id(p)] = cp
if self._symbolic_solver_labels:
for ndx, p in enumerate(params):
Expand Down Expand Up @@ -222,7 +223,8 @@ def _update_variables(self, variables: List[_GeneralVarData]):
def update_params(self):
for p_id, p in self._params.items():
cp = self._param_map[p_id]
cp.value = p.value
if p.value is not None:
cp.value = p.value

def set_objective(self, obj: _GeneralObjectiveData):
if self._symbolic_solver_labels:
Expand Down
1 change: 1 addition & 0 deletions pyomo/contrib/appsi/solvers/highs.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]):
del self._solver_con_to_pyomo_con_map[con_ndx]
indices_to_remove.append(con_ndx)
self._mutable_helpers.pop(con, None)
indices_to_remove.sort()
self._solver_model.deleteRows(
len(indices_to_remove), np.array(indices_to_remove)
)
Expand Down
43 changes: 43 additions & 0 deletions pyomo/contrib/coramin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Coramin

Coramin is a Pyomo-based Python package that provides tools for
developing tailored algorithms for mixed-integer nonlinear programming
problems (MINLP's). This software includes classes for managing and
refining convex relaxations of nonconvex constraints. These classes
provide methods for updating the relaxation based on new variable
bounds, creating and managing piecewise relaxations (for multi-tree
based algorithms), and adding outer-approximation based cuts for
convex or concave constraints. These classes inherit from Pyomo
Blocks, so they can be easily integrated with Pyomo
models. Additionally, Coramin has functions for automatically
generating convex relaxations of general Pyomo models. Coramin also
has tools for domain reduction, including a parallel implementation
of optimization-based bounds tightening (OBBT) and various OBBT
filtering techniques.

## Primary Contributors
### [Michael Bynum](https://github.com/michaelbynum)
- Relaxation classes
- OBBT
- OBBT Filtering
- Factorable programming approach to generating relaxations

### [Carl Laird](https://github.com/carldlaird)
- Parallel OBBT
- McCormick and piecewise McCormick relaxations for bilinear terms
- Relaxations for univariate convex/concave functions

### [Anya Castillo](https://github.com/anyacastillo)
- Relaxation classes

### [Francesco Ceccon](https://github.com/fracek)
- Alpha-BB relaxation

## Relevant Packages

### [Suspect](https://github.com/cog-imperial/suspect)
Use of Coramin can be improved significantly by also utilizing
Suspect's convexity detection and feasibility-based bounds tightening
features. Future development of Coramin will directly use Suspect in
Coramin's factorable programming approach to generating relaxations.

23 changes: 23 additions & 0 deletions pyomo/contrib/coramin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pyomo.common.dependencies import numpy, numpy_available, attempt_import
from pyomo.common import unittest

if not numpy_available:
raise unittest.SkipTest('numpy is not available')

pybnb, pybnb_available = attempt_import('pybnb')
if not pybnb_available:
raise unittest.SkipTest('pybnb is not available')

from . import utils
from . import domain_reduction
from . import relaxations
from . import algorithms
from . import third_party
from .utils import (
RelaxationSide,
FunctionShape,
Effort,
EigenValueBounder,
simplify_expr,
get_objective,
)
3 changes: 3 additions & 0 deletions pyomo/contrib/coramin/algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .ecp_bounder import ECPBounder
from .multitree.multitree import MultiTree
from .bnb.bnb import BnBSolver
89 changes: 89 additions & 0 deletions pyomo/contrib/coramin/algorithms/alg_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import pyomo.environ as pe
from pyomo.repn.standard_repn import generate_standard_repn, StandardRepn
from pyomo.contrib.coramin.relaxations.split_expr import split_expr
from pyomo.core.expr.numeric_expr import LinearExpression
from pyomo.core.base.var import _GeneralVarData
from pyomo.core.base.block import _BlockData
from pyomo.common.collections import ComponentSet
from typing import Tuple, List, Sequence


def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]:
binary_vars = ComponentSet()
integer_vars = ComponentSet()
for v in m.vars:
if v.is_binary():
binary_vars.add(v)
elif v.is_integer():
integer_vars.add(v)
return list(binary_vars), list(integer_vars)


def relax_integers(
binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]
):
for v in list(binary_vars) + list(integer_vars):
lb, ub = v.bounds
v.domain = pe.Reals
v.setlb(lb)
v.setub(ub)


def impose_structure(m):
m.aux_vars = pe.VarList()

for key, c in list(m.nonlinear.cons.items()):
repn: StandardRepn = generate_standard_repn(
c.body, quadratic=False, compute_values=True
)
expr_list = split_expr(repn.nonlinear_expr)
if len(expr_list) == 1:
continue

linear_coefs = list(repn.linear_coefs)
linear_vars = list(repn.linear_vars)
for term in expr_list:
v = m.aux_vars.add()
linear_coefs.append(1)
linear_vars.append(v)
m.vars.append(v)
if c.equality or (c.lb == c.ub and c.lb is not None):
m.nonlinear.cons.add(v == term)
elif c.ub is None:
m.nonlinear.cons.add(v <= term)
elif c.lb is None:
m.nonlinear.cons.add(v >= term)
else:
m.nonlinear.cons.add(v == term)
new_expr = LinearExpression(
constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars
)
m.linear.cons.add((c.lb, new_expr, c.ub))
del m.nonlinear.cons[key]

if hasattr(m.nonlinear, 'obj'):
obj = m.nonlinear.obj
repn: StandardRepn = generate_standard_repn(
obj.expr, quadratic=False, compute_values=True
)
expr_list = split_expr(repn.nonlinear_expr)
if len(expr_list) > 1:
linear_coefs = list(repn.linear_coefs)
linear_vars = list(repn.linear_vars)
for term in expr_list:
v = m.aux_vars.add()
linear_coefs.append(1)
linear_vars.append(v)
m.vars.append(v)
if obj.sense == pe.minimize:
m.nonlinear.cons.add(v >= term)
else:
assert obj.sense == pe.maximize
m.nonlinear.cons.add(v <= term)
new_expr = LinearExpression(
constant=repn.constant,
linear_coefs=linear_coefs,
linear_vars=linear_vars,
)
m.linear.obj = pe.Objective(expr=new_expr, sense=obj.sense)
del m.nonlinear.obj
Empty file.
Loading

0 comments on commit 54a0533

Please sign in to comment.