X-Git-Url: https://scm.cri.mines-paristech.fr/git/linpy.git/blobdiff_plain/2ffea1a47578a1b1d09906d57511062d68e6abea..960f0c252361dfd696359f803aae40a9b13b14a6:/pypol/linexprs.py diff --git a/pypol/linexprs.py b/pypol/linexprs.py index b330045..bd3ad5a 100644 --- a/pypol/linexprs.py +++ b/pypol/linexprs.py @@ -1,16 +1,33 @@ +# Copyright 2014 MINES ParisTech +# +# This file is part of Linpy. +# +# Linpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Linpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Linpy. If not, see . + import ast import functools import numbers import re -from collections import OrderedDict +from collections import OrderedDict, defaultdict, Mapping from fractions import Fraction, gcd __all__ = [ 'Expression', - 'Symbol', 'symbols', 'symbolname', 'symbolnames', - 'Constant', + 'Symbol', 'Dummy', 'symbols', + 'Rational', ] @@ -20,7 +37,7 @@ def _polymorphic(func): if isinstance(right, Expression): return func(left, right) elif isinstance(right, numbers.Rational): - right = Constant(right) + right = Rational(right) return func(left, right) return NotImplemented return wrapper @@ -31,68 +48,53 @@ class Expression: This class implements linear expressions. """ - __slots__ = ( - '_coefficients', - '_constant', - '_symbols', - '_dimension', - '_hash', - ) - def __new__(cls, coefficients=None, constant=0): if isinstance(coefficients, str): - if constant: + if constant != 0: raise TypeError('too many arguments') - return cls.fromstring(coefficients) - if isinstance(coefficients, dict): - coefficients = coefficients.items() + return Expression.fromstring(coefficients) if coefficients is None: - return Constant(constant) - coefficients = [(symbol, coefficient) - for symbol, coefficient in coefficients if coefficient != 0] + return Rational(constant) + if isinstance(coefficients, Mapping): + coefficients = coefficients.items() + coefficients = list(coefficients) + for symbol, coefficient in coefficients: + if not isinstance(symbol, Symbol): + raise TypeError('symbols must be Symbol instances') + if not isinstance(coefficient, numbers.Rational): + raise TypeError('coefficients must be rational numbers') + if not isinstance(constant, numbers.Rational): + raise TypeError('constant must be a rational number') if len(coefficients) == 0: - return Constant(constant) - elif len(coefficients) == 1 and constant == 0: + return Rational(constant) + if len(coefficients) == 1 and constant == 0: symbol, coefficient = coefficients[0] if coefficient == 1: - return Symbol(symbol) + return symbol + coefficients = [(symbol, Fraction(coefficient)) + for symbol, coefficient in coefficients if coefficient != 0] + coefficients.sort(key=lambda item: item[0].sortkey()) self = object().__new__(cls) - self._coefficients = {} - for symbol, coefficient in coefficients: - symbol = symbolname(symbol) - if isinstance(coefficient, Constant): - coefficient = coefficient.constant - if not isinstance(coefficient, numbers.Rational): - raise TypeError('coefficients must be rational numbers ' - 'or Constant instances') - self._coefficients[symbol] = coefficient - self._coefficients = OrderedDict(sorted(self._coefficients.items())) - if isinstance(constant, Constant): - constant = constant.constant - if not isinstance(constant, numbers.Rational): - raise TypeError('constant must be a rational number ' - 'or a Constant instance') - self._constant = constant + self._coefficients = OrderedDict(coefficients) + self._constant = Fraction(constant) self._symbols = tuple(self._coefficients) self._dimension = len(self._symbols) - self._hash = hash((tuple(self._coefficients.items()), self._constant)) return self def coefficient(self, symbol): - symbol = symbolname(symbol) - try: - return self._coefficients[symbol] - except KeyError: - return 0 + if not isinstance(symbol, Symbol): + raise TypeError('symbol must be a Symbol instance') + return Rational(self._coefficients.get(symbol, 0)) __getitem__ = coefficient def coefficients(self): - yield from self._coefficients.items() + for symbol, coefficient in self._coefficients.items(): + yield symbol, Rational(coefficient) @property def constant(self): - return self._constant + return Rational(self._constant) @property def symbols(self): @@ -103,7 +105,7 @@ class Expression: return self._dimension def __hash__(self): - return self._hash + return hash((tuple(self._coefficients.items()), self._constant)) def isconstant(self): return False @@ -112,9 +114,9 @@ class Expression: return False def values(self): - for symbol in self.symbols: - yield self.coefficient(symbol) - yield self.constant + for coefficient in self._coefficients.values(): + yield Rational(coefficient) + yield Rational(self._constant) def __bool__(self): return True @@ -127,103 +129,92 @@ class Expression: @_polymorphic def __add__(self, other): - coefficients = dict(self.coefficients()) - for symbol, coefficient in other.coefficients(): - if symbol in coefficients: - coefficients[symbol] += coefficient - else: - coefficients[symbol] = coefficient - constant = self.constant + other.constant + coefficients = defaultdict(Fraction, self._coefficients) + for symbol, coefficient in other._coefficients.items(): + coefficients[symbol] += coefficient + constant = self._constant + other._constant return Expression(coefficients, constant) __radd__ = __add__ @_polymorphic def __sub__(self, other): - coefficients = dict(self.coefficients()) - for symbol, coefficient in other.coefficients(): - if symbol in coefficients: - coefficients[symbol] -= coefficient - else: - coefficients[symbol] = -coefficient - constant = self.constant - other.constant + coefficients = defaultdict(Fraction, self._coefficients) + for symbol, coefficient in other._coefficients.items(): + coefficients[symbol] -= coefficient + constant = self._constant - other._constant return Expression(coefficients, constant) + @_polymorphic def __rsub__(self, other): - return -(self - other) + return other - self - @_polymorphic def __mul__(self, other): - if other.isconstant(): - coefficients = dict(self.coefficients()) - for symbol in coefficients: - coefficients[symbol] *= other.constant - constant = self.constant * other.constant + if isinstance(other, numbers.Rational): + coefficients = ((symbol, coefficient * other) + for symbol, coefficient in self._coefficients.items()) + constant = self._constant * other return Expression(coefficients, constant) - if isinstance(other, Expression) and not self.isconstant(): - raise ValueError('non-linear expression: ' - '{} * {}'.format(self._parenstr(), other._parenstr())) return NotImplemented __rmul__ = __mul__ - @_polymorphic def __truediv__(self, other): - if other.isconstant(): - coefficients = dict(self.coefficients()) - for symbol in coefficients: - coefficients[symbol] = \ - Fraction(coefficients[symbol], other.constant) - constant = Fraction(self.constant, other.constant) + if isinstance(other, numbers.Rational): + coefficients = ((symbol, coefficient / other) + for symbol, coefficient in self._coefficients.items()) + constant = self._constant / other return Expression(coefficients, constant) - if isinstance(other, Expression): - raise ValueError('non-linear expression: ' - '{} / {}'.format(self._parenstr(), other._parenstr())) - return NotImplemented - - def __rtruediv__(self, other): - if isinstance(other, self): - if self.isconstant(): - constant = Fraction(other, self.constant) - return Expression(constant=constant) - else: - raise ValueError('non-linear expression: ' - '{} / {}'.format(other._parenstr(), self._parenstr())) return NotImplemented @_polymorphic def __eq__(self, other): - # "normal" equality + # returns a boolean, not a constraint # see http://docs.sympy.org/dev/tutorial/gotchas.html#equals-signs return isinstance(other, Expression) and \ - self._coefficients == other._coefficients and \ - self.constant == other.constant + self._coefficients == other._coefficients and \ + self._constant == other._constant - @_polymorphic def __le__(self, other): from .polyhedra import Le return Le(self, other) - @_polymorphic def __lt__(self, other): from .polyhedra import Lt return Lt(self, other) - @_polymorphic def __ge__(self, other): from .polyhedra import Ge return Ge(self, other) - @_polymorphic def __gt__(self, other): from .polyhedra import Gt return Gt(self, other) - def _toint(self): + def scaleint(self): lcm = functools.reduce(lambda a, b: a*b // gcd(a, b), [value.denominator for value in self.values()]) return self * lcm + def subs(self, symbol, expression=None): + if expression is None: + if isinstance(symbol, Mapping): + symbol = symbol.items() + substitutions = symbol + else: + substitutions = [(symbol, expression)] + result = self + for symbol, expression in substitutions: + if not isinstance(symbol, Symbol): + raise TypeError('symbols must be Symbol instances') + coefficients = [(othersymbol, coefficient) + for othersymbol, coefficient in result._coefficients.items() + if othersymbol != symbol] + coefficient = result._coefficients.get(symbol, 0) + constant = result._constant + result = Expression(coefficients, constant) + coefficient*expression + return result + @classmethod def _fromast(cls, node): if isinstance(node, ast.Module) and len(node.body) == 1: @@ -233,7 +224,7 @@ class Expression: elif isinstance(node, ast.Name): return Symbol(node.id) elif isinstance(node, ast.Num): - return Constant(node.n) + return Rational(node.n) elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): return -cls._fromast(node.operand) elif isinstance(node, ast.BinOp): @@ -249,75 +240,63 @@ class Expression: return left / right raise SyntaxError('invalid syntax') - def subs(self, symbol, expression=None): - if expression is None: - if isinstance(symbol, dict): - symbol = symbol.items() - substitutions = symbol - else: - substitutions = [(symbol, expression)] - result = self - for symbol, expression in substitutions: - symbol = symbolname(symbol) - result = result._subs(symbol, expression) - return result - - def _subs(self, symbol, expression): - coefficients = {name: coefficient - for name, coefficient in self.coefficients() - if name != symbol} - constant = self.constant - coefficient = self.coefficient(symbol) - result = Expression(coefficients, self.constant) - result += coefficient * expression - return result - _RE_NUM_VAR = re.compile(r'(\d+|\))\s*([^\W\d_]\w*|\()') @classmethod def fromstring(cls, string): # add implicit multiplication operators, e.g. '5x' -> '5*x' - string = cls._RE_NUM_VAR.sub(r'\1*\2', string) + string = Expression._RE_NUM_VAR.sub(r'\1*\2', string) tree = ast.parse(string, 'eval') return cls._fromast(tree) - def __str__(self): + def __repr__(self): string = '' - i = 0 - for symbol in self.symbols: - coefficient = self.coefficient(symbol) + for i, (symbol, coefficient) in enumerate(self.coefficients()): if coefficient == 1: - if i == 0: - string += symbol - else: - string += ' + {}'.format(symbol) + if i != 0: + string += ' + ' elif coefficient == -1: - if i == 0: - string += '-{}'.format(symbol) - else: - string += ' - {}'.format(symbol) + string += '-' if i == 0 else ' - ' + elif i == 0: + string += '{}*'.format(coefficient) + elif coefficient > 0: + string += ' + {}*'.format(coefficient) else: - if i == 0: - string += '{}*{}'.format(coefficient, symbol) - elif coefficient > 0: - string += ' + {}*{}'.format(coefficient, symbol) - else: - assert coefficient < 0 - coefficient *= -1 - string += ' - {}*{}'.format(coefficient, symbol) - i += 1 + string += ' - {}*'.format(-coefficient) + string += '{}'.format(symbol) constant = self.constant - if constant != 0 and i == 0: + if len(string) == 0: string += '{}'.format(constant) elif constant > 0: string += ' + {}'.format(constant) elif constant < 0: - constant *= -1 - string += ' - {}'.format(constant) - if string == '': - string = '0' + string += ' - {}'.format(-constant) return string + def _repr_latex_(self): + string = '' + for i, (symbol, coefficient) in enumerate(self.coefficients()): + if coefficient == 1: + if i != 0: + string += ' + ' + elif coefficient == -1: + string += '-' if i == 0 else ' - ' + elif i == 0: + string += '{}'.format(coefficient._repr_latex_().strip('$')) + elif coefficient > 0: + string += ' + {}'.format(coefficient._repr_latex_().strip('$')) + elif coefficient < 0: + string += ' - {}'.format((-coefficient)._repr_latex_().strip('$')) + string += '{}'.format(symbol._repr_latex_().strip('$')) + constant = self.constant + if len(string) == 0: + string += '{}'.format(constant._repr_latex_().strip('$')) + elif constant > 0: + string += ' + {}'.format(constant._repr_latex_().strip('$')) + elif constant < 0: + string += ' - {}'.format((-constant)._repr_latex_().strip('$')) + return '$${}$$'.format(string) + def _parenstr(self, always=False): string = str(self) if not always and (self.isconstant() or self.issymbol()): @@ -325,30 +304,27 @@ class Expression: else: return '({})'.format(string) - def __repr__(self): - return '{}({!r})'.format(self.__class__.__name__, str(self)) - @classmethod def fromsympy(cls, expr): import sympy - coefficients = {} + coefficients = [] constant = 0 for symbol, coefficient in expr.as_coefficients_dict().items(): coefficient = Fraction(coefficient.p, coefficient.q) if symbol == sympy.S.One: constant = coefficient elif isinstance(symbol, sympy.Symbol): - symbol = symbol.name - coefficients[symbol] = coefficient + symbol = Symbol(symbol.name) + coefficients.append((symbol, coefficient)) else: raise ValueError('non-linear expression: {!r}'.format(expr)) - return cls(coefficients, constant) + return Expression(coefficients, constant) def tosympy(self): import sympy expr = 0 for symbol, coefficient in self.coefficients(): - term = coefficient * sympy.Symbol(symbol) + term = coefficient * sympy.Symbol(symbol.name) expr += term expr += self.constant return expr @@ -356,16 +332,15 @@ class Expression: class Symbol(Expression): - __slots__ = ( - '_name', - '_hash', - ) - def __new__(cls, name): - name = symbolname(name) + if not isinstance(name, str): + raise TypeError('name must be a string') self = object().__new__(cls) - self._name = name - self._hash = hash(self._name) + self._name = name.strip() + self._coefficients = {self: Fraction(1)} + self._constant = Fraction(0) + self._symbols = (self,) + self._dimension = 1 return self @property @@ -373,35 +348,19 @@ class Symbol(Expression): return self._name def __hash__(self): - return self._hash - - def coefficient(self, symbol): - symbol = symbolname(symbol) - if symbol == self.name: - return 1 - else: - return 0 - - def coefficients(self): - yield self.name, 1 + return hash(self.sortkey()) - @property - def constant(self): - return 0 - - @property - def symbols(self): + def sortkey(self): return self.name, - @property - def dimension(self): - return 1 - def issymbol(self): return True def __eq__(self, other): - return isinstance(other, Symbol) and self.name == other.name + return self.sortkey() == other.sortkey() + + def asdummy(self): + return Dummy(self.name) @classmethod def _fromast(cls, node): @@ -414,101 +373,107 @@ class Symbol(Expression): raise SyntaxError('invalid syntax') def __repr__(self): - return '{}({!r})'.format(self.__class__.__name__, self._name) + return self.name + + def _repr_latex_(self): + return '$${}$$'.format(self.name) @classmethod def fromsympy(cls, expr): import sympy - if isinstance(expr, sympy.Symbol): - return cls(expr.name) + if isinstance(expr, sympy.Dummy): + return Dummy(expr.name) + elif isinstance(expr, sympy.Symbol): + return Symbol(expr.name) else: raise TypeError('expr must be a sympy.Symbol instance') -def symbols(names): - if isinstance(names, str): - names = names.replace(',', ' ').split() - return (Symbol(name) for name in names) +class Dummy(Symbol): -def symbolname(symbol): - if isinstance(symbol, str): - return symbol.strip() - elif isinstance(symbol, Symbol): - return symbol.name - else: - raise TypeError('symbol must be a string or a Symbol instance') + _count = 0 -def symbolnames(symbols): - if isinstance(symbols, str): - return symbols.replace(',', ' ').split() - return (symbolname(symbol) for symbol in symbols) + def __new__(cls, name=None): + if name is None: + name = 'Dummy_{}'.format(Dummy._count) + elif not isinstance(name, str): + raise TypeError('name must be a string') + self = object().__new__(cls) + self._index = Dummy._count + self._name = name.strip() + self._coefficients = {self: Fraction(1)} + self._constant = Fraction(0) + self._symbols = (self,) + self._dimension = 1 + Dummy._count += 1 + return self + def __hash__(self): + return hash(self.sortkey()) + + def sortkey(self): + return self._name, self._index -class Constant(Expression): + def __repr__(self): + return '_{}'.format(self.name) - __slots__ = ( - '_constant', - '_hash', - ) + def _repr_latex_(self): + return '$${}_{{{}}}$$'.format(self.name, self._index) + + +def symbols(names): + if isinstance(names, str): + names = names.replace(',', ' ').split() + return tuple(Symbol(name) for name in names) + + +class Rational(Expression, Fraction): def __new__(cls, numerator=0, denominator=None): self = object().__new__(cls) - if denominator is None and isinstance(numerator, Constant): - self._constant = numerator.constant - else: - self._constant = Fraction(numerator, denominator) - self._hash = hash(self._constant) + self._coefficients = {} + self._constant = Fraction(numerator, denominator) + self._symbols = () + self._dimension = 0 + self._numerator = self._constant.numerator + self._denominator = self._constant.denominator return self def __hash__(self): - return self._hash - - def coefficient(self, symbol): - symbol = symbolname(symbol) - return 0 - - def coefficients(self): - yield from [] + return Fraction.__hash__(self) @property - def symbols(self): - return () - - @property - def dimension(self): - return 0 + def constant(self): + return self def isconstant(self): return True - @_polymorphic - def __eq__(self, other): - return isinstance(other, Constant) and self.constant == other.constant - def __bool__(self): - return self.constant != 0 - - @classmethod - def fromstring(cls, string): - if isinstance(string, str): - return Constant(Fraction(string)) - else: - raise TypeError('string must be a string instance') + return Fraction.__bool__(self) def __repr__(self): - if self.constant.denominator == 1: - return '{}({!r})'.format(self.__class__.__name__, - self.constant.numerator) + if self.denominator == 1: + return '{!r}'.format(self.numerator) + else: + return '{!r}/{!r}'.format(self.numerator, self.denominator) + + def _repr_latex_(self): + if self.denominator == 1: + return '$${}$$'.format(self.numerator) + elif self.numerator < 0: + return '$$-\\frac{{{}}}{{{}}}$$'.format(-self.numerator, + self.denominator) else: - return '{}({!r}, {!r})'.format(self.__class__.__name__, - self.constant.numerator, self.constant.denominator) + return '$$\\frac{{{}}}{{{}}}$$'.format(self.numerator, + self.denominator) @classmethod def fromsympy(cls, expr): import sympy if isinstance(expr, sympy.Rational): - return cls(expr.p, expr.q) + return Rational(expr.p, expr.q) elif isinstance(expr, numbers.Rational): - return cls(expr) + return Rational(expr) else: raise TypeError('expr must be a sympy.Rational instance')