PySCIPOpt
Python Interface to the SCIP Optimization Suite
expr.pxi
Go to the documentation of this file.
1 ##@file expr.pxi
2 #@brief In this file we implemenet the handling of expressions
3 #@details We have two types of expressions: Expr and GenExpr.
4 # The Expr can only handle polynomial expressions.
5 # In addition, one can recover easily information from them.
6 # A polynomial is a dictionary between `terms` and coefficients.
7 # A `term` is a tuple of variables
8 # For examples, 2*x*x*y*z - 1.3 x*y*y + 1 is stored as a
9 # {Term(x,x,y,z) : 2, Term(x,y,y) : -1.3, Term() : 1}
10 # Addition of common terms and expansion of exponents occur automatically.
11 # Given the way `Expr`s are stored, it is easy to access the terms: e.g.
12 # expr = 2*x*x*y*z - 1.3 x*y*y + 1
13 # expr[Term(x,x,y,z)] returns 1.3
14 # expr[Term(x)] returns 0.0
15 #
16 # On the other hand, when dealing with expressions more general than polynomials,
17 # that is, absolute values, exp, log, sqrt or any general exponent, we use GenExpr.
18 # GenExpr stores expression trees in a rudimentary way.
19 # Basically, it stores the operator and the list of children.
20 # We have different types of general expressions that in addition
21 # to the operation and list of children stores
22 # SumExpr: coefficients and constant
23 # ProdExpr: constant
24 # Constant: constant
25 # VarExpr: variable
26 # PowExpr: exponent
27 # UnaryExpr: nothing
28 # We do not provide any way of accessing the internal information of the expression tree,
29 # nor we simplify common terms or do any other type of simplification.
30 # The `GenExpr` is pass as is to SCIP and SCIP will do what it see fits during presolving.
31 #
32 # TODO: All this is very complicated, so we might wanna unify Expr and GenExpr.
33 # Maybe when consexpr is released it makes sense to revisit this.
34 # TODO: We have to think about the operations that we define: __isub__, __add__, etc
35 # and when to copy expressions and when to not copy them.
36 # For example: when creating a ExprCons from an Expr expr, we store the expression expr
37 # and then we normalize. When doing the normalization, we do
38 # ```
39 # c = self.expr[CONST]
40 # self.expr -= c
41 # ```
42 # which should, in princple, modify the expr. However, since we do not implement __isub__, __sub__
43 # gets called (I guess) and so a copy is returned.
44 # Modifying the expression directly would be a bug, given that the expression might be re-used by the user.
45 
46 
47 def _is_number(e):
48  try:
49  f = float(e)
50  return True
51  except ValueError: # for malformed strings
52  return False
53  except TypeError: # for other types (Variable, Expr)
54  return False
55 
56 
57 def _expr_richcmp(self, other, op):
58  if op == 1: # <=
59  if isinstance(other, Expr) or isinstance(other, GenExpr):
60  return (self - other) <= 0.0
61  elif _is_number(other):
62  return ExprCons(self, rhs=float(other))
63  else:
64  raise NotImplementedError
65  elif op == 5: # >=
66  if isinstance(other, Expr) or isinstance(other, GenExpr):
67  return (self - other) >= 0.0
68  elif _is_number(other):
69  return ExprCons(self, lhs=float(other))
70  else:
71  raise NotImplementedError
72  elif op == 2: # ==
73  if isinstance(other, Expr) or isinstance(other, GenExpr):
74  return (self - other) == 0.0
75  elif _is_number(other):
76  return ExprCons(self, lhs=float(other), rhs=float(other))
77  else:
78  raise NotImplementedError
79  else:
80  raise NotImplementedError
81 
82 
83 class Term:
84  '''This is a monomial term'''
85 
86  __slots__ = ('vartuple', 'ptrtuple', 'hashval')
87 
88  def __init__(self, *vartuple):
89  self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr()))
90  self.ptrtuple = tuple(v.ptr() for v in self.vartuple)
91  self.hashval = sum(self.ptrtuple)
92 
93  def __getitem__(self, idx):
94  return self.vartuple[idx]
95 
96  def __hash__(self):
97  return self.hashval
98 
99  def __eq__(self, other):
100  return self.ptrtuple == other.ptrtuple
101 
102  def __len__(self):
103  return len(self.vartuple)
104 
105  def __add__(self, other):
106  both = self.vartuple + other.vartuple
107  return Term(*both)
108 
109  def __repr__(self):
110  return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple])
111 
112 CONST = Term()
113 
114 # helper function
115 def buildGenExprObj(expr):
116  """helper function to generate an object of type GenExpr"""
117  if _is_number(expr):
118  return Constant(expr)
119  elif isinstance(expr, Expr):
120  # loop over terms and create a sumexpr with the sum of each term
121  # each term is either a variable (which gets transformed into varexpr)
122  # or a product of variables (which gets tranformed into a prod)
123  sumexpr = SumExpr()
124  for vars, coef in expr.terms.items():
125  if len(vars) == 0:
126  sumexpr += coef
127  elif len(vars) == 1:
128  varexpr = VarExpr(vars[0])
129  sumexpr += coef * varexpr
130  else:
131  prodexpr = ProdExpr()
132  for v in vars:
133  varexpr = VarExpr(v)
134  prodexpr *= varexpr
135  sumexpr += coef * prodexpr
136  return sumexpr
137  else:
138  assert isinstance(expr, GenExpr)
139  return expr
140 
141 cdef class Expr:
142  '''Polynomial expressions of variables with operator overloading.'''
143  cdef public terms
144 
145  def __init__(self, terms=None):
146  '''terms is a dict of variables to coefficients.
147 
148  CONST is used as key for the constant term.'''
149  self.terms = {} if terms is None else terms
150 
151  if len(self.terms) == 0:
152  self.terms[CONST] = 0.0
153 
154  def __getitem__(self, key):
155  if not isinstance(key, Term):
156  key = Term(key)
157  return self.terms.get(key, 0.0)
158 
159  def __iter__(self):
160  return iter(self.terms)
161 
162  def __next__(self):
163  try: return next(self.terms)
164  except: raise StopIteration
165 
166  def __abs__(self):
167  return abs(buildGenExprObj(self))
168 
169  def __add__(self, other):
170  left = self
171  right = other
172 
173  if _is_number(self):
174  assert isinstance(other, Expr)
175  left,right = right,left
176  terms = left.terms.copy()
177 
178  if isinstance(right, Expr):
179  # merge the terms by component-wise addition
180  for v,c in right.terms.items():
181  terms[v] = terms.get(v, 0.0) + c
182  elif _is_number(right):
183  c = float(right)
184  terms[CONST] = terms.get(CONST, 0.0) + c
185  elif isinstance(right, GenExpr):
186  return buildGenExprObj(left) + right
187  else:
188  raise NotImplementedError
189  return Expr(terms)
190 
191  def __iadd__(self, other):
192  if isinstance(other, Expr):
193  for v,c in other.terms.items():
194  self.terms[v] = self.terms.get(v, 0.0) + c
195  elif _is_number(other):
196  c = float(other)
197  self.terms[CONST] = self.terms.get(CONST, 0.0) + c
198  elif isinstance(other, GenExpr):
199  # is no longer in place, might affect performance?
200  # can't do `self = buildGenExprObj(self) + other` since I get
201  # TypeError: Cannot convert pyscipopt.scip.SumExpr to pyscipopt.scip.Expr
202  return buildGenExprObj(self) + other
203  else:
204  raise NotImplementedError
205  return self
206 
207  def __mul__(self, other):
208  if _is_number(other):
209  f = float(other)
210  return Expr({v:f*c for v,c in self.terms.items()})
211  elif _is_number(self):
212  f = float(self)
213  return Expr({v:f*c for v,c in other.terms.items()})
214  elif isinstance(other, Expr):
215  terms = {}
216  for v1, c1 in self.terms.items():
217  for v2, c2 in other.terms.items():
218  v = v1 + v2
219  terms[v] = terms.get(v, 0.0) + c1 * c2
220  return Expr(terms)
221  elif isinstance(other, GenExpr):
222  return buildGenExprObj(self) * other
223  else:
224  raise NotImplementedError
225 
226  def __div__(self, other):
227  ''' transforms Expr into GenExpr'''
228  if _is_number(other):
229  f = 1.0/float(other)
230  return f * self
231  selfexpr = buildGenExprObj(self)
232  return selfexpr.__div__(other)
233 
234  def __rdiv__(self, other):
235  ''' other / self '''
236  if _is_number(self):
237  f = 1.0/float(self)
238  return f * other
239  otherexpr = buildGenExprObj(other)
240  return otherexpr.__div__(self)
241 
242  def __truediv__(self,other):
243  if _is_number(other):
244  f = 1.0/float(other)
245  return f * self
246  selfexpr = buildGenExprObj(self)
247  return selfexpr.__truediv__(other)
248 
249  def __rtruediv__(self, other):
250  ''' other / self '''
251  if _is_number(self):
252  f = 1.0/float(self)
253  return f * other
254  otherexpr = buildGenExprObj(other)
255  return otherexpr.__truediv__(self)
256 
257  def __pow__(self, other, modulo):
258  if float(other).is_integer() and other >= 0:
259  exp = int(other)
260  else: # need to transform to GenExpr
261  return buildGenExprObj(self)**other
262 
263  res = 1
264  for _ in range(exp):
265  res *= self
266  return res
267 
268  def __neg__(self):
269  return Expr({v:-c for v,c in self.terms.items()})
270 
271  def __sub__(self, other):
272  return self + (-other)
273 
274  def __radd__(self, other):
275  return self.__add__(other)
276 
277  def __rmul__(self, other):
278  return self.__mul__(other)
279 
280  def __rsub__(self, other):
281  return -1.0 * self + other
282 
283  def __richcmp__(self, other, op):
284  '''turn it into a constraint'''
285  return _expr_richcmp(self, other, op)
286 
287  def normalize(self):
288  '''remove terms with coefficient of 0'''
289  self.terms = {t:c for (t,c) in self.terms.items() if c != 0.0}
290 
291  def __repr__(self):
292  return 'Expr(%s)' % repr(self.terms)
293 
294  def degree(self):
295  '''computes highest degree of terms'''
296  if len(self.terms) == 0:
297  return 0
298  else:
299  return max(len(v) for v in self.terms)
300 
301 
302 cdef class ExprCons:
303  '''Constraints with a polynomial expressions and lower/upper bounds.'''
304  cdef public expr
305  cdef public lhs
306  cdef public rhs
307 
308  def __init__(self, expr, lhs=None, rhs=None):
309  self.expr = expr
310  self.lhs = lhs
311  self.rhs = rhs
312  assert not (lhs is None and rhs is None)
313  self.normalize()
314 
315  def normalize(self):
316  '''move constant terms in expression to bounds'''
317  if isinstance(self.expr, Expr):
318  c = self.expr[CONST]
319  self.expr -= c
320  assert self.expr[CONST] == 0.0
321  self.expr.normalize()
322  else:
323  assert isinstance(self.expr, GenExpr)
324  return
325 
326  if not self.lhs is None:
327  self.lhs -= c
328  if not self.rhs is None:
329  self.rhs -= c
330 
331 
332  def __richcmp__(self, other, op):
333  '''turn it into a constraint'''
334  if op == 1: # <=
335  if not self.rhs is None:
336  raise TypeError('ExprCons already has upper bound')
337  assert self.rhs is None
338  assert not self.lhs is None
339 
340  if not _is_number(other):
341  raise TypeError('Ranged ExprCons is not well defined!')
342 
343  return ExprCons(self.expr, lhs=self.lhs, rhs=float(other))
344  elif op == 5: # >=
345  if not self.lhs is None:
346  raise TypeError('ExprCons already has lower bound')
347  assert self.lhs is None
348  assert not self.rhs is None
349 
350  if not _is_number(other):
351  raise TypeError('Ranged ExprCons is not well defined!')
352 
353  return ExprCons(self.expr, lhs=float(other), rhs=self.rhs)
354  else:
355  raise TypeError
356 
357  def __repr__(self):
358  return 'ExprCons(%s, %s, %s)' % (self.expr, self.lhs, self.rhs)
359 
360  def __nonzero__(self):
361  '''Make sure that equality of expressions is not asserted with =='''
362 
363  msg = """Can't evaluate constraints as booleans.
364 
365 If you want to add a ranged constraint of the form
366  lhs <= expression <= rhs
367 you have to use parenthesis to break the Python syntax for chained comparisons:
368  lhs <= (expression <= rhs)
369 """
370  raise TypeError(msg)
371 
372 def quicksum(termlist):
373  '''add linear expressions and constants much faster than Python's sum
374  by avoiding intermediate data structures and adding terms inplace
375  '''
376  result = Expr()
377  for term in termlist:
378  result += term
379  return result
380 
381 def quickprod(termlist):
382  '''multiply linear expressions and constants by avoiding intermediate
383  data structures and multiplying terms inplace
384  '''
385  result = Expr() + 1
386  for term in termlist:
387  result *= term
388  return result
389 
390 
391 class Op:
392  const = 'const'
393  varidx = 'var'
394  exp, log, sqrt = 'exp','log', 'sqrt'
395  plus, minus, mul, div, power = '+', '-', '*', '/', '**'
396  add = 'sum'
397  prod = 'prod'
398  fabs = 'abs'
399  operatorIndexDic={
400  varidx:SCIP_EXPR_VARIDX,
401  const:SCIP_EXPR_CONST,
402  plus:SCIP_EXPR_PLUS,
403  minus:SCIP_EXPR_MINUS,
404  mul:SCIP_EXPR_MUL,
405  div:SCIP_EXPR_DIV,
406  sqrt:SCIP_EXPR_SQRT,
407  power:SCIP_EXPR_REALPOWER,
408  exp:SCIP_EXPR_EXP,
409  log:SCIP_EXPR_LOG,
410  fabs:SCIP_EXPR_ABS,
411  add:SCIP_EXPR_SUM,
412  prod:SCIP_EXPR_PRODUCT
413  }
414  def getOpIndex(self, op):
415  '''returns operator index'''
416  return Op.operatorIndexDic[op];
417 
418 Operator = Op()
419 
420 cdef class GenExpr:
421  '''General expressions of variables with operator overloading.
422 
423  Notes:
424  - this expressions are not smart enough to identify equal terms
425  - in constrast to polynomial expressions, __getitem__ is not implemented
426  so expr[x] will generate an error instead of returning the coefficient of x
427  '''
428  cdef public operatorIndex
429  cdef public op
430  cdef public children
431 
432 
433  def __init__(self): # do we need it
434  ''' '''
435 
436  def __abs__(self):
437  return UnaryExpr(Operator.fabs, self)
438 
439  def __add__(self, other):
440  left = buildGenExprObj(self)
441  right = buildGenExprObj(other)
442  ans = SumExpr()
443 
444  # add left term
445  if left.getOp() == Operator.add:
446  ans.coefs.extend(left.coefs)
447  ans.children.extend(left.children)
448  ans.constant += left.constant
449  elif left.getOp() == Operator.const:
450  ans.constant += left.number
451  else:
452  ans.coefs.append(1.0)
453  ans.children.append(left)
454 
455  # add right term
456  if right.getOp() == Operator.add:
457  ans.coefs.extend(right.coefs)
458  ans.children.extend(right.children)
459  ans.constant += right.constant
460  elif right.getOp() == Operator.const:
461  ans.constant += right.number
462  else:
463  ans.coefs.append(1.0)
464  ans.children.append(right)
465 
466  return ans
467 
468  #def __iadd__(self, other):
469  #''' in-place addition, i.e., expr += other '''
470  # assert isinstance(self, Expr)
471  # right = buildGenExprObj(other)
472  #
473  # # transform self into sum
474  # if self.getOp() != Operator.add:
475  # newsum = SumExpr()
476  # if self.getOp() == Operator.const:
477  # newsum.constant += self.number
478  # else:
479  # newsum.coefs.append(1.0)
480  # newsum.children.append(self.copy()) # TODO: what is copy?
481  # self = newsum
482  # # add right term
483  # if right.getOp() == Operator.add:
484  # self.coefs.extend(right.coefs)
485  # self.children.extend(right.children)
486  # self.constant += right.constant
487  # elif right.getOp() == Operator.const:
488  # self.constant += right.number
489  # else:
490  # self.coefs.append(1.0)
491  # self.children.append(right)
492  # return self
493 
494  def __mul__(self, other):
495  left = buildGenExprObj(self)
496  right = buildGenExprObj(other)
497  ans = ProdExpr()
498 
499  # multiply left factor
500  if left.getOp() == Operator.prod:
501  ans.children.extend(left.children)
502  ans.constant *= left.constant
503  elif left.getOp() == Operator.const:
504  ans.constant *= left.number
505  else:
506  ans.children.append(left)
507 
508  # multiply right factor
509  if right.getOp() == Operator.prod:
510  ans.children.extend(right.children)
511  ans.constant *= right.constant
512  elif right.getOp() == Operator.const:
513  ans.constant *= right.number
514  else:
515  ans.children.append(right)
516 
517  return ans
518 
519  #def __imul__(self, other):
520  #''' in-place multiplication, i.e., expr *= other '''
521  # assert isinstance(self, Expr)
522  # right = buildGenExprObj(other)
523  # # transform self into prod
524  # if self.getOp() != Operator.prod:
525  # newprod = ProdExpr()
526  # if self.getOp() == Operator.const:
527  # newprod.constant *= self.number
528  # else:
529  # newprod.children.append(self.copy()) # TODO: what is copy?
530  # self = newprod
531  # # multiply right factor
532  # if right.getOp() == Operator.prod:
533  # self.children.extend(right.children)
534  # self.constant *= right.constant
535  # elif right.getOp() == Operator.const:
536  # self.constant *= right.number
537  # else:
538  # self.children.append(right)
539  # return self
540 
541  def __pow__(self, other, modulo):
542  expo = buildGenExprObj(other)
543  if expo.getOp() != Operator.const:
544  raise NotImplementedError("exponents must be numbers")
545  if self.getOp() == Operator.const:
546  return Constant(self.number**expo.number)
547  ans = PowExpr()
548  ans.children.append(self)
549  ans.expo = expo.number
550 
551  return ans
552 
553  #TODO: ipow, idiv, etc
554  def __div__(self, other):
555  divisor = buildGenExprObj(other)
556  # we can't divide by 0
557  if divisor.getOp() == Operator.const and divisor.number == 0.0:
558  raise ZeroDivisionError("cannot divide by 0")
559  return self * divisor**(-1)
560 
561  def __rdiv__(self, other):
562  ''' other / self '''
563  otherexpr = buildGenExprObj(other)
564  return otherexpr.__div__(self)
565 
566  def __truediv__(self,other):
567  divisor = buildGenExprObj(other)
568  # we can't divide by 0
569  if divisor.getOp() == Operator.const and divisor.number == 0.0:
570  raise ZeroDivisionError("cannot divide by 0")
571  return self * divisor**(-1)
572 
573  def __rtruediv__(self, other):
574  ''' other / self '''
575  otherexpr = buildGenExprObj(other)
576  return otherexpr.__truediv__(self)
577 
578  def __neg__(self):
579  return -1.0 * self
580 
581  def __sub__(self, other):
582  return self + (-other)
583 
584  def __radd__(self, other):
585  return self.__add__(other)
586 
587  def __rmul__(self, other):
588  return self.__mul__(other)
589 
590  def __rsub__(self, other):
591  return -1.0 * self + other
592 
593  def __richcmp__(self, other, op):
594  '''turn it into a constraint'''
595  return _expr_richcmp(self, other, op)
596 
597  def degree(self):
598  '''Note: none of these expressions should be polynomial'''
599  return float('inf')
600 
601  def getOp(self):
602  '''returns operator of GenExpr'''
603  return self.op
604 
605 
606 # Sum Expressions
607 cdef class SumExpr(GenExpr):
608 
609  cdef public constant
610  cdef public coefs
611 
612  def __init__(self):
613  self.constant = 0.0
614  self.coefs = []
615  self.children = []
616  self.op = Operator.add
617  self.operatorIndex = Operator.operatorIndexDic[self.op]
618  def __repr__(self):
619  return self.op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"
620 
621 # Prod Expressions
622 cdef class ProdExpr(GenExpr):
623  cdef public constant
624  def __init__(self):
625  self.constant = 1.0
626  self.children = []
627  self.op = Operator.prod
628  self.operatorIndex = Operator.operatorIndexDic[self.op]
629  def __repr__(self):
630  return self.op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"
631 
632 # Var Expressions
633 cdef class VarExpr(GenExpr):
634  cdef public var
635  def __init__(self, var):
636  self.children = [var]
637  self.op = Operator.varidx
638  self.operatorIndex = Operator.operatorIndexDic[self.op]
639  def __repr__(self):
640  return self.children[0].__repr__()
641 
642 # Pow Expressions
643 cdef class PowExpr(GenExpr):
644  cdef public expo
645  def __init__(self):
646  self.expo = 1.0
647  self.children = []
648  self.op = Operator.power
649  self.operatorIndex = Operator.operatorIndexDic[self.op]
650  def __repr__(self):
651  return self.op + "(" + self.children[0].__repr__() + "," + str(self.expo) + ")"
652 
653 # Exp, Log, Sqrt Expressions
654 cdef class UnaryExpr(GenExpr):
655  def __init__(self, op, expr):
656  self.children = []
657  self.children.append(expr)
658  self.op = op
659  self.operatorIndex = Operator.operatorIndexDic[op]
660  def __repr__(self):
661  return self.op + "(" + self.children[0].__repr__() + ")"
662 
663 # class for constant expressions
664 cdef class Constant(GenExpr):
665  cdef public number
666  def __init__(self,number):
667  self.number = number
668  self.op = Operator.const
669  self.operatorIndex = Operator.operatorIndexDic[self.op]
670 
671  def __repr__(self):
672  return str(self.number)
673 
674 def exp(expr):
675  """returns expression with exp-function"""
676  return UnaryExpr(Operator.exp, buildGenExprObj(expr))
677 def log(expr):
678  """returns expression with log-function"""
679  return UnaryExpr(Operator.log, buildGenExprObj(expr))
680 def sqrt(expr):
681  """returns expression with sqrt-function"""
682  return UnaryExpr(Operator.sqrt, buildGenExprObj(expr))
683 
684 def expr_to_nodes(expr):
685  '''transforms tree to an array of nodes. each node is an operator and the position of the
686  children of that operator (i.e. the other nodes) in the array'''
687  assert isinstance(expr, GenExpr)
688  nodes = []
689  expr_to_array(expr, nodes)
690  return nodes
691 
692 def value_to_array(val, nodes):
693  """adds a given value to an array"""
694  nodes.append(tuple(['const', [val]]))
695  return len(nodes) - 1
696 
697 # there many hacky things here: value_to_array is trying to mimick
698 # the multiple dispatch of julia. Also that we have to ask which expression is which
699 # in order to get the constants correctly
700 # also, for sums, we are not considering coefficients, because basically all coefficients are 1
701 # haven't even consider substractions, but I guess we would interpret them as a - b = a + (-1) * b
702 def expr_to_array(expr, nodes):
703  """adds expression to array"""
704  op = expr.op
705  if op == Operator.const: # FIXME: constant expr should also have children!
706  nodes.append(tuple([op, [expr.number]]))
707  elif op != Operator.varidx:
708  indices = []
709  nchildren = len(expr.children)
710  for child in expr.children:
711  pos = expr_to_array(child, nodes) # position of child in the final array of nodes, 'nodes'
712  indices.append(pos)
713  if op == Operator.power:
714  pos = value_to_array(expr.expo, nodes)
715  indices.append(pos)
716  elif (op == Operator.add and expr.constant != 0.0) or (op == Operator.prod and expr.constant != 1.0):
717  pos = value_to_array(expr.constant, nodes)
718  indices.append(pos)
719  nodes.append( tuple( [op, indices] ) )
720  else: # var
721  nodes.append( tuple( [op, expr.children] ) )
722  return len(nodes) - 1
def degree(self)
Definition: expr.pxi:294
def degree(self)
Definition: expr.pxi:597
def getOp(self)
Definition: expr.pxi:601
def normalize(self)
Definition: expr.pxi:287
def getOpIndex(self, op)
Definition: expr.pxi:414
def normalize(self)
Definition: expr.pxi:315