commit f438a8146bf15ac6185325053c020634cf02a901 Author: Mikaël Capelle Date: Wed Oct 30 19:37:08 2019 +0100 [simplex] Initial commit. diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..51b50a0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 100 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d98256 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.pyc +**/__pycache__ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82be88a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019, Mikaël Capelle. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b542b6 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Simplex dictionary + +Small package to manipulate a simplex dictionary in python. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..03e6d92 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- + +import setuptools + +with open('README.md', 'r') as fh: + long_description = fh.read() + +setuptools.setup( + name="simplex", # Replace with your own username + version="0.0.1", + author='Mikaël Capelle', + author_email='capelle.mikael@gmail.com', + description='Implementation of a simplex dictionary in python', + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/mikael.capelle/simplex", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.5', +) diff --git a/simplex/__init__.py b/simplex/__init__.py new file mode 100644 index 0000000..07a8177 --- /dev/null +++ b/simplex/__init__.py @@ -0,0 +1,5 @@ +# -*- encoding: utf-8 -*- + +# flake8: noqa + +from .simplex_dictionary import simplex_dictionary diff --git a/simplex/magic_dictionary.py b/simplex/magic_dictionary.py new file mode 100644 index 0000000..3476252 --- /dev/null +++ b/simplex/magic_dictionary.py @@ -0,0 +1,77 @@ +# -*- encoding: utf-8 -*- + + +class magic_dictionary(dict): + """ + A magic dictionary is a dictionary that: + - May contain a default value, much like collections.defaultdict. + - Raise custom errors when the given key does not match a given + predicate. + - Can convert values using a specific functor. + """ + + def __init__(self, *args, key_predicate=None, value_converter=None, default_factory=None, **kwargs): + """ Create a new magic dictionary from the given arguments. Magic dictionary can be constructed + in the same way as standard python dictionary, except that the constructor takes three extra + parameters. + + Parameters: + - key_predicate Predicate to apply on new key, which should return None if the key is valid, + or a message indicating why the key is invalid otherwize. The predicates is not applied on + the initial keys of the dictionary (from args or kwargs). Can be None. + - value_converter Functor to apply to values in this dictionary, when set by the index operator. + This functor is also applied to all initial values of the dictionary. Can be None. + - default_facotry Default factory to use to create object when the key is not set. Can be None. + """ + + super().__init__(*args, **kwargs) + + self.key_predicate = key_predicate + self.value_converter = value_converter + self.default_factory = default_factory + + # Copy construction: + if len(args) >= 1 and isinstance(args[0], magic_dictionary): + if key_predicate is None: + self.key_predicate = args[0].key_predicate + if value_converter is None: + self.value_converter = args[0].value_converter + if default_factory is None: + self.default_factory = args[0].default_factory + + # Check the value: + if self.key_predicate is not None: + for k in self: + check = self.key_predicate(k) + if check is not None: + raise IndexError(check) + + # Convert the value: + if self.value_converter is not None: + for k in self: + super().__setitem__(k, self.value_converter(self[k])) + + def __getitem__(self, key): + + # If the key does not exists: + if key not in self: + # We try to set it to a new object: + if self.default_factory is not None: + self.__setitem__(key, self.default_factory()) + + + # Return the key: + return super().__getitem__(key) + + def __setitem__(self, key, value): + # If the key predicate is not empty, we check the key: + if self.key_predicate is not None: + check = self.key_predicate(key) + if check is not None: + raise KeyError(check) + + # Convert the value: + if self.value_converter is not None: + value = self.value_converter(value) + + super().__setitem__(key, value) diff --git a/simplex/simplex_dictionary.py b/simplex/simplex_dictionary.py new file mode 100644 index 0000000..fe98ea0 --- /dev/null +++ b/simplex/simplex_dictionary.py @@ -0,0 +1,144 @@ +from IPython.display import display, Math +from fractions import Fraction + +from .magic_dictionary import magic_dictionary + + +def convert_value(value): + if type(value) is float: + # For float value, we don't handle float: + raise TypeError('Cannot set value as float, use fractions.Fraction instead.') + return Fraction(value) + + +class simplex_dictionary: + + """ Class representing a dictionary for the simplex algorithm. The class contains + multiple members representing the elements of a dictionary. + + Members: + - B The list of basic variables (immutable). + - N The list of non-basic variables (immutable). + - b The current value of the basic variables. + - a The current coefficients of the non-basic variables in the expression + of the basic variables. + - c The current coefficients of the non-basic variables in the objective. + - z The current value of the objective. + + The magic member ".variables" can also be used to access the list of all + variables. + + The various arrays of coefficients or values (b, a, c) are indexed by their respective + variables, which must be in B or N. + + All values are converted to fractions.Fraction object in order to maintain consistency + in the dictionary. + + Examples: + # Create a simplex dictionary with two basic and two non-basic variables: + >>> sdict = simplex_dictionary(B=['x_1', 'x_2'], N=['x_3', 'x_4']) + + # Set the value of x_1: + >>> sdict.b['x_1'] = 4 + + # Set the coefficient of x_3 in x_1: + >>> sdict.a['x_1']['x_3'] = 12 + """ + + def __init__(self, B, N): + """ Create a new simplex dictionary with the given basic and non-basic variables. + + Parameters: + - B The list of basic variables. + - N The list of non-basic variables. + """ + self.__B = tuple(B) + self.__N = tuple(N) + self.b = {} + self.a = {} + self.c = {} + self.z = 0 + + def _check_basic(self, key): + """ Check if the given key is a basic variable, returning None if it is, + and a custom exception string if not. Suitable for magic_dictionary use. """ + if key not in self.B: + return '{} is not a basic variable.'.format(key) + return None + + def _check_non_basic(self, key): + """ Check if the given key is a non-basic variable, returning None if it is, + and a custom exception string if not. Suitable for magic_dictionary use. """ + if key not in self.N: + return '{} is not a non-basic variable.'.format(key) + return None + + @property + def variables(self): return sorted(self.B + self.N) + + @property + def B(self): return self.__B + + @property + def N(self): return self.__N + + def __setattr__(self, key, value): + + # Base and non-basic: + if key == 'b': + value = magic_dictionary( + value, + key_predicate=self._check_basic, + value_converter=convert_value) + elif key == 'c': + value = magic_dictionary( + value, + key_predicate=self._check_non_basic, + value_converter=convert_value) + elif key == 'a': + value = magic_dictionary( + value, + key_predicate=self._check_basic, + value_converter=lambda value: magic_dictionary( + value, key_predicate=self._check_non_basic, + value_converter=convert_value), + default_factory=lambda: magic_dictionary( + key_predicate=self._check_non_basic, + value_converter=convert_value)) + elif key == 'z': + value = convert_value(value) + + return super().__setattr__(key, value) + + def value_latex(self, value): + if value.denominator == 1: + return str(value) + return r'{}\frac{{{}}}{{{}}}'.format( + '-' if value.numerator < 0 else '', + abs(value.numerator), value.denominator) + + def display(self, name=None): + """ Display this simplex dictionary on the standard Jupyter output. + + Parameters: + - prefix Name of the dictionary. + """ + d = (r'\begin{{array}}{{r||{}}}'.format( + 'r|' * (1 + len(self.B))) + + r' & b & ' + ' & '.join(str(v) for v in self.N) + r'\\\hline ' + + r'\\'.join( + '{} & {} &'.format( + b, + self.value_latex(self.b[b]) + ) + + ' & '.join(self.value_latex(-self.a[b][n]) for n in self.N) + for i, b in enumerate(self.B)) + + r'\\\hline\hline ' + + r'&'.join(['z', self.value_latex(self.z)] + [self.value_latex(self.c[n]) + for n in self.N]) + + r'\\\hline\end{array}') + + if name is not None: + d = r'{} = \left.{}\right.'.format(name, d) + + display(Math(d))