Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints and type loop variables #946

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions src/pyscipopt/benders.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ cdef SCIP_RETCODE PyBendersPostsolve (SCIP* scip, SCIP_BENDERS* benders, SCIP_SO
SCIP_BENDERSENFOTYPE type, int* mergecands, int npriomergecands, int nmergecands, SCIP_Bool checkint,
SCIP_Bool infeasible, SCIP_Bool* merged) noexcept with gil:
cdef SCIP_BENDERSDATA* bendersdata
cdef int i

bendersdata = SCIPbendersGetData(benders)
PyBenders = <Benders>bendersdata
if sol == NULL:
Expand Down
6 changes: 3 additions & 3 deletions src/pyscipopt/branchrule.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ cdef class Branchrule:
'''informs branching rule that the branch and bound process data is being freed'''
pass

def branchexeclp(self, allowaddcons):
def branchexeclp(self, SCIP_Bool allowaddcons):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these methods not defined with cdef? I would say in the python world we should use bool and in the c world SCIP_Bool. So where are we here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As I said in the bigger comment, these functions should be accessible from the Python side, so we need to use def.

Copy link
Contributor

Choose a reason for hiding this comment

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

But I guess to avoid confusion we should also use bool instead of SCIP_Bool and convert when entering the C world again.

Copy link
Collaborator

@Opt-Mucca Opt-Mucca Jan 22, 2025

Choose a reason for hiding this comment

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

SCIP_Bool does not make sense here. The user should never have access to such a type and the type hint should simply be bool (like Dominik suggested).
Cython will handle the conversion or use a local declaration of SCIP_Bool inside of the function.

'''executes branching rule for fractional LP solution'''
raise NotImplementedError("branchexeclp() is a fundamental callback and should be implemented in the derived "
"class")

def branchexecext(self, allowaddcons):
def branchexecext(self, SCIP_Bool allowaddcons):
'''executes branching rule for external branching candidates '''
raise NotImplementedError("branchexecext() is a fundamental callback and should be implemented in the derived class")

def branchexecps(self, allowaddcons):
def branchexecps(self, SCIP_Bool allowaddcons):
'''executes branching rule for not completely fixed pseudo solution '''
# this method needs to be implemented by the user
raise NotImplementedError("branchexecps() is a fundamental callback and should be implemented in the derived class")
Expand Down
56 changes: 36 additions & 20 deletions src/pyscipopt/conshdlr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cdef class Conshdlr:
'''informs constraint handler that the branch and bound process is being started '''
pass

def consexitsol(self, constraints, restart):
def consexitsol(self, constraints, SCIP_Bool restart):
Copy link
Contributor

Choose a reason for hiding this comment

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

What about constraints?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I wasn't sure if they were allowed to be just one or not and was too lazy to check

Copy link
Contributor

Choose a reason for hiding this comment

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

If this is the list of constraints, I would not allow it to be a single constraint, can we declare it an iterable?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You can declare something as an iterable in Python type hints. We might need to then import the collections library

'''informs constraint handler that the branch and bound process data is being freed '''
pass

Expand All @@ -45,74 +45,74 @@ cdef class Conshdlr:
'''calls LP initialization method of constraint handler to separate all initial active constraints '''
return {}

def conssepalp(self, constraints, nusefulconss):
def conssepalp(self, constraints, int nusefulconss):
'''calls separator method of constraint handler to separate LP solution '''
return {}

def conssepasol(self, constraints, nusefulconss, solution):
def conssepasol(self, constraints, int nusefulconss, Solution solution):
'''calls separator method of constraint handler to separate given primal solution '''
return {}

def consenfolp(self, constraints, nusefulconss, solinfeasible):
def consenfolp(self, constraints, int nusefulconss, SCIP_Bool solinfeasible):
'''calls enforcing method of constraint handler for LP solution for all constraints added'''
print("python error in consenfolp: this method needs to be implemented")
return {}

def consenforelax(self, solution, constraints, nusefulconss, solinfeasible):
def consenforelax(self, Solution solution, constraints, int nusefulconss, solinfeasible):
'''calls enforcing method of constraint handler for a relaxation solution for all constraints added'''
print("python error in consenforelax: this method needs to be implemented")
return {}

def consenfops(self, constraints, nusefulconss, solinfeasible, objinfeasible):
def consenfops(self, constraints, int nusefulconss, SCIP_Bool solinfeasible, SCIP_Bool objinfeasible):
'''calls enforcing method of constraint handler for pseudo solution for all constraints added'''
print("python error in consenfops: this method needs to be implemented")
return {}

def conscheck(self, constraints, solution, checkintegrality, checklprows, printreason, completely):
def conscheck(self, constraints, Solution solution, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool printreason, SCIP_Bool completely):
'''calls feasibility check method of constraint handler '''
print("python error in conscheck: this method needs to be implemented")
return {}

def consprop(self, constraints, nusefulconss, nmarkedconss, proptiming):
def consprop(self, constraints, int nusefulconss, int nmarkedconss, SCIP_PROPTIMING proptiming):
'''calls propagation method of constraint handler '''
return {}

def conspresol(self, constraints, nrounds, presoltiming,
nnewfixedvars, nnewaggrvars, nnewchgvartypes, nnewchgbds, nnewholes,
nnewdelconss, nnewaddconss, nnewupgdconss, nnewchgcoefs, nnewchgsides, result_dict):
def conspresol(self, constraints, int nrounds, SCIP_PRESOLTIMING presoltiming,
int nnewfixedvars, int nnewaggrvars, int nnewchgvartypes, int nnewchgbds, int nnewholes,
int nnewdelconss, int nnewaddconss, int nnewupgdconss, int nnewchgcoefs, int nnewchgsides, result_dict):
'''calls presolving method of constraint handler '''
return result_dict

def consresprop(self):
'''sets propagation conflict resolving method of constraint handler '''
return {}

def conslock(self, constraint, locktype, nlockspos, nlocksneg):
def conslock(self, constraint: Union[Constraint, ExprCons], int locktype, int nlockspos, int nlocksneg):
'''variable rounding lock method of constraint handler'''
print("python error in conslock: this method needs to be implemented")
return {}

def consactive(self, constraint):
def consactive(self, constraint: Union[Constraint, ExprCons]):
'''sets activation notification method of constraint handler '''
pass

def consdeactive(self, constraint):
def consdeactive(self, constraint: Union[Constraint, ExprCons]):
'''sets deactivation notification method of constraint handler '''
pass

def consenable(self, constraint):
def consenable(self, constraint: Union[Constraint, ExprCons]):
'''sets enabling notification method of constraint handler '''
pass

def consdisable(self, constraint):
def consdisable(self, constraint: Union[Constraint, ExprCons]):
'''sets disabling notification method of constraint handler '''
pass

def consdelvars(self, constraints):
def consdelvars(self, constraint: Union[Constraint, ExprCons]):
'''calls variable deletion method of constraint handler'''
pass

def consprint(self, constraint):
def consprint(self, constraint: Union[Constraint, ExprCons]):
'''sets constraint display method of constraint handler '''
pass

Expand All @@ -124,11 +124,11 @@ cdef class Conshdlr:
'''sets constraint parsing method of constraint handler '''
pass

def consgetvars(self, constraint):
def consgetvars(self, constraint: Union[Constraint, ExprCons]):
'''sets constraint variable getter method of constraint handler'''
pass

def consgetnvars(self, constraint):
def consgetnvars(self, constraint: Union[Constraint, ExprCons]):
'''sets constraint variable number getter method of constraint handler '''
return {}

Expand Down Expand Up @@ -170,6 +170,7 @@ cdef SCIP_RETCODE PyConsFree (SCIP* scip, SCIP_CONSHDLR* conshdlr) noexcept with
cdef SCIP_RETCODE PyConsInit (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
PyConshdlr.consinit(constraints)
Expand All @@ -178,6 +179,7 @@ cdef SCIP_RETCODE PyConsInit (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** c
cdef SCIP_RETCODE PyConsExit (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
PyConshdlr.consexit(constraints)
Expand All @@ -186,6 +188,7 @@ cdef SCIP_RETCODE PyConsExit (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** c
cdef SCIP_RETCODE PyConsInitpre (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
PyConshdlr.consinitpre(constraints)
Expand All @@ -194,6 +197,7 @@ cdef SCIP_RETCODE PyConsInitpre (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS*
cdef SCIP_RETCODE PyConsExitpre (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
PyConshdlr.consexitpre(constraints)
Expand All @@ -202,6 +206,7 @@ cdef SCIP_RETCODE PyConsExitpre (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS*
cdef SCIP_RETCODE PyConsInitsol (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
PyConshdlr.consinitsol(constraints)
Expand All @@ -210,6 +215,7 @@ cdef SCIP_RETCODE PyConsInitsol (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS*
cdef SCIP_RETCODE PyConsExitsol (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, SCIP_Bool restart) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
PyConshdlr.consexitsol(constraints, restart)
Expand Down Expand Up @@ -246,6 +252,7 @@ cdef SCIP_RETCODE PyConsTrans (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* s
cdef SCIP_RETCODE PyConsInitlp (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, SCIP_Bool* infeasible) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
result_dict = PyConshdlr.consinitlp(constraints)
Expand All @@ -255,6 +262,7 @@ cdef SCIP_RETCODE PyConsInitlp (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS**
cdef SCIP_RETCODE PyConsSepalp (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
result_dict = PyConshdlr.conssepalp(constraints, nusefulconss)
Expand All @@ -265,6 +273,7 @@ cdef SCIP_RETCODE PyConsSepasol (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS*
SCIP_SOL* sol, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
solution = Solution.create(scip, sol)
Expand All @@ -276,6 +285,7 @@ cdef SCIP_RETCODE PyConsEnfolp (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS**
SCIP_Bool solinfeasible, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
result_dict = PyConshdlr.consenfolp(constraints, nusefulconss, solinfeasible)
Expand All @@ -285,6 +295,7 @@ cdef SCIP_RETCODE PyConsEnfolp (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS**
cdef SCIP_RETCODE PyConsEnforelax (SCIP* scip, SCIP_SOL* sol, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_Bool solinfeasible, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
solution = Solution.create(scip, sol)
Expand All @@ -296,6 +307,7 @@ cdef SCIP_RETCODE PyConsEnfops (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS**
SCIP_Bool solinfeasible, SCIP_Bool objinfeasible, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
result_dict = PyConshdlr.consenfops(constraints, nusefulconss, solinfeasible, objinfeasible)
Expand All @@ -306,6 +318,7 @@ cdef SCIP_RETCODE PyConsCheck (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS**
SCIP_Bool checklprows, SCIP_Bool printreason, SCIP_Bool completely, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
solution = Solution.create(scip, sol)
Expand All @@ -317,6 +330,7 @@ cdef SCIP_RETCODE PyConsProp (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** c
SCIP_PROPTIMING proptiming, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
result_dict = PyConshdlr.consprop(constraints, nusefulconss, nmarkedconss, proptiming)
Expand All @@ -330,6 +344,7 @@ cdef SCIP_RETCODE PyConsPresol (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS**
int* ndelconss, int* naddconss, int* nupgdconss, int* nchgcoefs, int* nchgsides, SCIP_RESULT* result) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
# dictionary for input/output parameters
Expand Down Expand Up @@ -403,6 +418,7 @@ cdef SCIP_RETCODE PyConsDisable (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS*
cdef SCIP_RETCODE PyConsDelvars (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss) noexcept with gil:
PyConshdlr = getPyConshdlr(conshdlr)
cdef constraints = []
cdef int i
for i in range(nconss):
constraints.append(getPyCons(conss[i]))
PyConshdlr.consdelvars(constraints)
Expand Down
5 changes: 3 additions & 2 deletions src/pyscipopt/cutsel.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ cdef class Cutsel:
'''executed before the branch-and-bound process is freed'''
pass

def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts):
def cutselselect(self, list[SCIP_ROW] cuts, int forcedcuts, SCIP_Bool root, int maxnselectedcuts):
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess if we have def then the int means the Python int whereas SCIP_Bool is the C unsigned int, or am I wrong?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not entirely sure how this works, because in things like addCons I type all the options as SCIP_Bools, but then assign them Python booleans. I guess Cython makes some kind of automatic conversion?

Copy link
Contributor

@DominikKamp DominikKamp Jan 17, 2025

Choose a reason for hiding this comment

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

Maybe I am not aware of the Cython rules, but I find these type declarations confusing because

  1. list[SCIP_ROW] seems to be a Python-container of a C-type
  2. int is the C-int
  3. SCIP_Bool is the C-uint

and since this should be a Python-callback, I would prefer if it is consistently supplied with Python-objects.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm also not sure about Cython, but I think there must be a definitive difference between this callback and a normal Python function. That's because using this sort of syntax (e.g. int forcedcuts) leads to an invalid syntax error.

Type hints in Python would use the following syntax (and not be enforced, making them somewhat useless):

def cutselselect(self, cuts: list[SCIP_ROW] , forcedcuts: int, root: bool, maxnselectedcuts: int):

Further, in these callbacks, bool is not recognized as a type identifier.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess we're mixing type hints and variable type declarations here.
Regardless: list[SCIP_ROW] would be incorrect because the callback receives list[Row], where one in theory could then access row._scip_row, which has type SCIP_ROW.

'''first method called in each iteration in the main solving loop. '''
# this method needs to be implemented by the user
return {}
Expand Down Expand Up @@ -74,9 +74,10 @@ cdef SCIP_RETCODE PyCutselSelect (SCIP* scip, SCIP_CUTSEL* cutsel, SCIP_ROW** cu
int* nselectedcuts, SCIP_RESULT* result) noexcept with gil:
cdef SCIP_CUTSELDATA* cutseldata
cdef SCIP_ROW* scip_row
cdef int i
cutseldata = SCIPcutselGetData(cutsel)
PyCutsel = <Cutsel>cutseldata

# translate cuts to python
pycuts = [Row.create(cuts[i]) for i in range(ncuts)]
pyforcedcuts = [Row.create(forcedcuts[i]) for i in range(nforcedcuts)]
Expand Down
19 changes: 10 additions & 9 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _is_number(e):
return False


def _expr_richcmp(self, other, op):
def _expr_richcmp(self, other, int op):
if op == 1: # <=
if isinstance(other, Expr) or isinstance(other, GenExpr):
return (self - other) <= 0.0
Expand Down Expand Up @@ -90,7 +90,7 @@ class Term:
self.ptrtuple = tuple(v.ptr() for v in self.vartuple)
self.hashval = sum(self.ptrtuple)

def __getitem__(self, idx):
def __getitem__(self, int idx):
return self.vartuple[idx]

def __hash__(self):
Expand All @@ -115,6 +115,7 @@ CONST = Term()
# helper function
def buildGenExprObj(expr):
"""helper function to generate an object of type GenExpr"""
cdef SCIP_Real coef
if _is_number(expr):
return Constant(expr)
elif isinstance(expr, Expr):
Expand Down Expand Up @@ -143,7 +144,7 @@ def buildGenExprObj(expr):
#See also the @ref ExprDetails "description" in the expr.pxi.
cdef class Expr:

def __init__(self, terms=None):
def __init__(self, terms: Union[None, dict[Variable,SCIP_Real]] = None):
'''terms is a dict of variables to coefficients.

CONST is used as key for the constant term.'''
Expand Down Expand Up @@ -620,23 +621,23 @@ cdef class Constant(GenExpr):
def __repr__(self):
return str(self.number)

def exp(expr):
def exp(expr: Union[Expr, GenExpr]):
"""returns expression with exp-function"""
return UnaryExpr(Operator.exp, buildGenExprObj(expr))
def log(expr):
def log(expr: Union[Expr, GenExpr]):
"""returns expression with log-function"""
return UnaryExpr(Operator.log, buildGenExprObj(expr))
def sqrt(expr):
def sqrt(expr: Union[Expr, GenExpr]):
"""returns expression with sqrt-function"""
return UnaryExpr(Operator.sqrt, buildGenExprObj(expr))
def sin(expr):
def sin(expr: Union[Expr, GenExpr]):
"""returns expression with sin-function"""
return UnaryExpr(Operator.sin, buildGenExprObj(expr))
def cos(expr):
def cos(expr: Union[Expr, GenExpr]):
"""returns expression with cos-function"""
return UnaryExpr(Operator.cos, buildGenExprObj(expr))

def expr_to_nodes(expr):
def expr_to_nodes(expr: Union[Expr, GenExpr]):
'''transforms tree to an array of nodes. each node is an operator and the position of the
children of that operator (i.e. the other nodes) in the array'''
assert isinstance(expr, GenExpr)
Expand Down
2 changes: 1 addition & 1 deletion src/pyscipopt/heuristic.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cdef class Heur:
'''informs primal heuristic that the branch and bound process data is being freed'''
pass

def heurexec(self, heurtiming, nodeinfeasible):
def heurexec(self, SCIP_HEURTIMING heurtiming, SCIP_Bool nodeinfeasible):
'''should the heuristic the executed at the given depth, frequency, timing,...'''
print("python error in heurexec: this method needs to be implemented")
return {}
Expand Down
Loading
Loading