Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
### Added
- Added recipe for getting local constraints
- More support for AND-Constraints
- Added support for knapsack constraints
- Added isPositive(), isNegative(), isFeasLE(), isFeasLT(), isFeasGE(), isFeasGT(), isHugeValue(), and tests
Expand Down
50 changes: 50 additions & 0 deletions src/pyscipopt/recipes/getLocalConss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from pyscipopt import Model, Constraint

def getLocalConss(model: Model, node = None) -> list[Constraint]:
Comment thread
Joao-Dionisio marked this conversation as resolved.
Outdated
"""
Returns local constraints.

Parameters
----------
model : Model
The model from which to retrieve the local constraints.
node : Node, optional
The node from which to retrieve the local constraints. If not provided, the current node is used.

Returns
-------
list[Constraint]
A list of local constraints. First entry are global constraints, second entry are all the added constraints.
"""

if not node:
Comment thread
Joao-Dionisio marked this conversation as resolved.
Outdated
assert model.getStageName() in ["INITPRESOLVE", "PRESOLVING", "EXITPRESOLVE", "SOLVING"], "Model cannot be called in stage %s." % model.getStageName()
cur_node = model.getCurrentNode()
else:
cur_node = node

added_conss = []
while cur_node is not None:
added_conss = cur_node.getAddedConss() + added_conss
cur_node = cur_node.getParent()

return [model.getConss(), added_conss]

def getNLocalConss(model: Model, node = None) -> list[int]:
Copy link

Copilot AI Jul 26, 2025

Choose a reason for hiding this comment

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

The return type annotation should be -> tuple[int, int] or -> list[int] to be more precise, but the current annotation is consistent with the actual return behavior.

Suggested change
def getNLocalConss(model: Model, node = None) -> list[int]:
def getNLocalConss(model: Model, node = None) -> tuple[int, int]:

Copilot uses AI. Check for mistakes.
"""
Returns the number of local constraints of a node.

Parameters
----------
model : Model
The model from which to retrieve the number of local constraints.
node : Node, optional
The node from which to retrieve the number of local constraints. If not provided, the current node is used.

Returns
-------
list[int]
A list of the number of local constraints. First entry is the number of global constraints, second entry is the number of all the added constraints.
"""
local_conss = getLocalConss(model, node)
return [len(local_conss[0]), len(local_conss[1])]
1 change: 0 additions & 1 deletion src/pyscipopt/recipes/nonlinear.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from pyscipopt import Model


def set_nonlinear_objective(model: Model, expr, sense="minimize"):
"""
Takes a nonlinear expression and performs an epigraph reformulation.
Expand Down
1 change: 1 addition & 0 deletions src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ cdef extern from "scip/scip.h":
SCIP_RETCODE SCIPwriteOrigProblem(SCIP* scip, char* filename, char* extension, SCIP_Bool genericnames)
SCIP_RETCODE SCIPwriteTransProblem(SCIP* scip, char* filename, char* extension, SCIP_Bool genericnames)
SCIP_RETCODE SCIPwriteLP(SCIP* scip, const char*)
SCIP_RETCODE SCIPwriteMIP(SCIP * scip, const char * filename, SCIP_Bool genericnames, SCIP_Bool origobj, SCIP_Bool lazyconss)
SCIP_STATUS SCIPgetStatus(SCIP* scip)
SCIP_Real SCIPepsilon(SCIP* scip)
SCIP_Real SCIPfeastol(SCIP* scip)
Expand Down
23 changes: 23 additions & 0 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

Check failure on line 304 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!

Check failure on line 304 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand Down Expand Up @@ -9665,6 +9665,29 @@
PY_SCIP_CALL( SCIPwriteLP(self._scip, absfile) )

locale.setlocale(locale.LC_NUMERIC,user_locale)

def writeMIP(self, filename, genericnames=False, origobj=False, lazyconss=False):
"""
Writes MIP relaxation of the current branch-and-bound node to a file

Parameters
----------
filename : str
name of the output file
genericnames : bool, optional
should generic names like x_i and row_j be used in order to avoid troubles with reserved symbols? (Default value = False)
origobj : bool, optional
should the original objective function be used (Default value = False)
lazyconss : bool, optional
output removable rows as lazy constraints? (Default value = False)
"""
user_locale = locale.getlocale(category=locale.LC_NUMERIC)
locale.setlocale(locale.LC_NUMERIC, "C")

absfile = str_conversion(abspath(filename))
PY_SCIP_CALL(SCIPwriteMIP(self._scip, absfile, genericnames, origobj, lazyconss))

locale.setlocale(locale.LC_NUMERIC,user_locale)

def createSol(self, Heur heur = None, initlp=False):
"""
Expand Down
38 changes: 38 additions & 0 deletions tests/test_recipe_getLocalConss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from pyscipopt import Model, SCIP_EVENTTYPE
from pyscipopt.recipes.getLocalConss import *
from helpers.utils import random_mip_1

def localconss(model, event):
local_conss = getLocalConss(model)
assert len(local_conss[1]) == getNLocalConss(model)[1]
assert len(local_conss[0]) == len(model.getConss())
assert local_conss[0] == model.getConss()

vars = model.getVars()
if model.getCurrentNode().getNumber() == 1:
Comment thread
Joao-Dionisio marked this conversation as resolved.
pass

elif model.getCurrentNode().getNumber() == 2:
model.data["local_cons1"] = model.addCons(vars[0] + vars[1] <= 1, name="c1", local=True)
assert getNLocalConss(model)[1] == 1
assert getLocalConss(model)[1][0] == model.data["local_cons1"]

elif model.getCurrentNode().getNumber() == 4:
local_conss = getLocalConss(model)
model.data["local_cons2"] = model.addCons(vars[1] + vars[2] <= 1, name="c2", local=True)
model.data["local_cons3"] = model.addCons(vars[2] + vars[3] <= 1, name="c3", local=True)
assert getNLocalConss(model)[1] == 3
assert getLocalConss(model)[1][0] == model.data["local_cons1"]
assert getLocalConss(model)[1][1] == model.data["local_cons2"]
assert getLocalConss(model)[1][2] == model.data["local_cons3"]

elif model.getCurrentNode().getParent().getNumber() not in [2,4]:
Comment thread
Joao-Dionisio marked this conversation as resolved.
assert getLocalConss(model) == [model.getConss(), []]

def test_getLocalConss():
model = random_mip_1(node_lim=4)
model.data = {}

model.attachEventHandlerCallback(localconss, [SCIP_EVENTTYPE.NODEFOCUSED])
model.optimize()
assert len(model.data) == 3