├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── pypareto ├── __init__.py └── pypareto.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 kummahiih 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # pypareto 3 | Pypareto is a Python library for pareto front seaching. 4 | ## Usage 5 | ComparisonChain.split_by_pareto performs the pareto front split fronts. 6 | 7 | Currently this works only for unique rows. You can add id as the last row (and not sort by it) to work around this restriction. 8 | 9 | Here the None means just inferior value: 10 | 11 | >>> values = [(0,None,None), (2,2,2), (0,1,1), (0,0,1), (None,0,1), (0,1,0), (None,1,1), (1,0,0), (0,0,0)] 12 | >>> chain = Comparison(by_value, MaxMinList(MaxMin.MAX, MaxMin.MAX, MaxMin.MAX)).as_chain() 13 | >>> chain.split_by_pareto(values) 14 | [[(2, 2, 2)], [(0, 1, 1), (1, 0, 0)], [(0, 0, 1), (0, 1, 0), (None, 1, 1)], [(None, 0, 1), (0, 0, 0)], [(0, None, None)]] 15 | 16 | Here one extra None means that the whole row is inferior: 17 | 18 | >>> values = [(0,None,None), (2,2,2), (0,1,1), (0,0,1), (None,0,1), (0,1,0), (None,1,1), (1,0,0), (0,0,0), (None, 0, None)] 19 | >>> chain = GroupNones(MaxMinList(MaxMin.MIN, MaxMin.MIN, MaxMin.MIN)).and_then( 20 | ... Comparison(by_value, MaxMinList(MaxMin.MAX, MaxMin.MAX, MaxMin.MAX))) 21 | >>> chain.split_by_pareto(values) 22 | [[(2, 2, 2)], [(0, 1, 1), (1, 0, 0)], [(0, 0, 1), (0, 1, 0)], [(0, 0, 0)], [(None, 1, 1)], [(None, 0, 1)], [(0, None, None), (None, 0, None)]] 23 | 24 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-leap-day -------------------------------------------------------------------------------- /pypareto/__init__.py: -------------------------------------------------------------------------------- 1 | from .pypareto import ( 2 | Cmp, 3 | Comparison, 4 | ComparisonChain, 5 | Domination, 6 | MaxMin, 7 | MaxMinList, 8 | by_none, 9 | by_value, 10 | cmp_to_target, 11 | dominates, 12 | GroupNones) 13 | 14 | __doc__ = """ 15 | # pypareto 16 | Pypareto is a Python library for pareto front seaching. 17 | ## Usage 18 | """ + ComparisonChain.split_by_pareto.__doc__ 19 | 20 | __all__ = [ 21 | 'Cmp', 22 | 'Comparison', 23 | 'GroupNones', 24 | 'ComparisonChain', 25 | 'Domination', 26 | 'MaxMin', 27 | 'MaxMinList', 28 | 'by_none', 29 | 'by_value', 30 | 'cmp_to_target', 31 | 'dominates', 32 | 'DominanceMatrix'] -------------------------------------------------------------------------------- /pypareto/pypareto.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2020 Pauli Henrikki Rikula 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | """ 10 | 11 | from typing import List 12 | from enum import Enum, auto 13 | import abc 14 | from collections import defaultdict, Counter 15 | 16 | class MaxMin(Enum): 17 | MAX = 1 18 | MIN = 2 19 | SKIP = 3 20 | 21 | def __repr__(self) -> str: 22 | return self.name 23 | 24 | class MaxMinList: 25 | def __init__(self, *target_list: List[MaxMin], none_is_good=False): 26 | """ 27 | >>> MaxMinList(MaxMin.MAX, MaxMin.MIN) 28 | MaxMinList[MAX ,MIN] 29 | """ 30 | self._target_list = target_list 31 | self._dim = len(target_list) 32 | self._none_is_good = none_is_good 33 | 34 | def __repr__(self) -> str: 35 | return "MaxMinList[{}]".format(" ,".join([i.__repr__() for i in self._target_list])) 36 | 37 | @property 38 | def dim(self) -> int: return self._dim 39 | 40 | @property 41 | def list(self) -> List[MaxMin]: 42 | return self._target_list 43 | 44 | @property 45 | def none_is_good(self): 46 | return self._none_is_good 47 | 48 | class Domination(Enum): 49 | GREATER = 1 50 | EQUAL = 0 51 | LESS = -1 52 | 53 | def __repr__(self) -> str: 54 | return self.name 55 | 56 | 57 | def by_none(a, b) -> Domination: 58 | """ 59 | >>> by_none(None, 1) 60 | GREATER 61 | >>> by_none(1, None) 62 | LESS 63 | >>> by_none(None, None) 64 | EQUAL 65 | >>> by_none(1, 1) 66 | EQUAL 67 | 68 | """ 69 | if a is None and b is not None: 70 | return Domination.GREATER 71 | if b is None and a is not None: 72 | return Domination.LESS 73 | return Domination.EQUAL 74 | 75 | def by_value(a, b) -> Domination: 76 | """ 77 | >>> by_value(2, 1) 78 | GREATER 79 | >>> by_value(1, 2) 80 | LESS 81 | >>> by_value(2, 2) 82 | EQUAL 83 | >>> by_value(1, 1) 84 | EQUAL 85 | 86 | """ 87 | if a > b: 88 | return Domination.GREATER 89 | if a < b: 90 | return Domination.LESS 91 | return Domination.EQUAL 92 | 93 | def cmp_to_target(a, b, cmp, target: MaxMin, none_is_good) -> Domination: 94 | """ 95 | >>> cmp_to_target(2, 1, by_value, MaxMin.MAX, False) 96 | GREATER 97 | >>> cmp_to_target(1, 2, by_value, MaxMin.MAX, False) 98 | LESS 99 | >>> cmp_to_target(2, 2, by_value, MaxMin.MAX, False) 100 | EQUAL 101 | >>> cmp_to_target(1, 1, by_value, MaxMin.MAX, False) 102 | EQUAL 103 | >>> cmp_to_target(2, 1, by_value, MaxMin.MIN, False) 104 | LESS 105 | >>> cmp_to_target(1, 2, by_value, MaxMin.MIN, False) 106 | GREATER 107 | >>> cmp_to_target(2, 2, by_value, MaxMin.MIN, False) 108 | EQUAL 109 | >>> cmp_to_target(1, 2, by_value, MaxMin.SKIP, False) 110 | EQUAL 111 | >>> cmp_to_target(None, 1, by_value, MaxMin.MIN, False) 112 | LESS 113 | >>> cmp_to_target(1, None, by_value, MaxMin.MIN, False) 114 | GREATER 115 | >>> cmp_to_target(None, None, by_value, MaxMin.MIN, False) 116 | EQUAL 117 | >>> cmp_to_target(None, 1, by_none, MaxMin.MAX, False) 118 | LESS 119 | >>> cmp_to_target(1, None, by_none, MaxMin.MAX, False) 120 | GREATER 121 | >>> cmp_to_target(None, None, by_none, MaxMin.MAX, False) 122 | EQUAL 123 | 124 | """ 125 | if target is MaxMin.SKIP: 126 | return Domination.EQUAL 127 | if a is None and b is not None: 128 | return Domination.LESS if not none_is_good else Domination.GREATER 129 | if a is not None and b is None: 130 | return Domination.GREATER if not none_is_good else Domination.LESS 131 | 132 | if a is None and b is None: 133 | return Domination.EQUAL 134 | 135 | cmp_result = cmp(a ,b) 136 | if target is MaxMin.MAX: 137 | return cmp_result 138 | else: 139 | if cmp_result is Domination.LESS: 140 | return Domination.GREATER 141 | elif cmp_result is Domination.GREATER: 142 | return Domination.LESS 143 | else: 144 | return Domination.EQUAL 145 | 146 | def dominates(a: list, b: list, cmp, targets: MaxMinList) -> Domination: 147 | """ 148 | >>> a = [None] 149 | >>> b = [1] 150 | >>> target = MaxMinList(MaxMin.MIN) 151 | >>> dominates(a, a, by_none, target) 152 | EQUAL 153 | >>> dominates(a, b, by_none, target) 154 | LESS 155 | >>> dominates(b, a, by_none, target) 156 | GREATER 157 | >>> dominates(b, b, by_none, target) 158 | EQUAL 159 | >>> a = [1, None] 160 | >>> b = [1, 1] 161 | >>> target = MaxMinList(MaxMin.MIN, MaxMin.MIN) 162 | >>> dominates(a, a, by_none, target) 163 | EQUAL 164 | >>> dominates(a, b, by_none, target) 165 | LESS 166 | >>> dominates(b, a, by_none, target) 167 | GREATER 168 | >>> dominates(b, b, by_none, target) 169 | EQUAL 170 | >>> dominates([1, None], [None, 1], by_none, target) 171 | EQUAL 172 | >>> a = [2, 1] 173 | >>> b = [2, 2] 174 | >>> target = MaxMinList(MaxMin.MAX, MaxMin.MAX) 175 | >>> dominates(a, a, by_value, target) 176 | EQUAL 177 | >>> dominates(a, b, by_value, target) 178 | LESS 179 | >>> dominates(b, a, by_value, target) 180 | GREATER 181 | >>> dominates(b, b, by_value, target) 182 | EQUAL 183 | >>> dominates([1, 0], [0, 1], by_value, target) 184 | EQUAL 185 | >>> dominates((None, 0, 1), (0, 0, 0), by_value, MaxMinList(MaxMin.MAX, MaxMin.MAX, MaxMin.MAX, none_is_good=False)) 186 | EQUAL 187 | 188 | 189 | """ 190 | results = list() 191 | for d in range(targets.dim): 192 | results.append(cmp_to_target(a[d], b[d], cmp, targets.list[d], targets.none_is_good)) 193 | 194 | is_greater_in_any = False 195 | for r in results: 196 | if r is Domination.GREATER: 197 | is_greater_in_any = True 198 | break 199 | 200 | is_less_in_any = False 201 | for r in results: 202 | if r is Domination.LESS: 203 | is_less_in_any = True 204 | break 205 | 206 | if is_greater_in_any and not is_less_in_any: 207 | return Domination.GREATER 208 | if is_less_in_any and not is_greater_in_any: 209 | return Domination.LESS 210 | return Domination.EQUAL 211 | 212 | 213 | class DominanceMatrix: 214 | """ 215 | Dominance matrix used in the fast non dominated search from: 216 | 217 | Verma G., Kumar A., Mishra K.K. (2011) A Novel Non-dominated Sorting Algorithm. 218 | In: Panigrahi B.K., Suganthan P.N., Das S., Satapathy S.C. (eds) Swarm, Evolutionary, and Memetic Computing. 219 | SEMCCO 2011. Lecture Notes in Computer Science, vol 7076. Springer, Berlin, Heidelberg 220 | 221 | 222 | >>> targets = MaxMinList(MaxMin.MAX, MaxMin.MAX, MaxMin.MAX) 223 | >>> dominates = Comparison(by_value, targets).compare 224 | >>> values = [(2,2,2), (0,1,1), (0,0,1), (0,1,0), (1,0,0), (0,0,0)] 225 | >>> for front in DominanceMatrix(values, dominates).get_pareto_fronts(): 226 | ... print(front) 227 | [(2, 2, 2)] 228 | [(0, 1, 1), (1, 0, 0)] 229 | [(0, 0, 1), (0, 1, 0)] 230 | [(0, 0, 0)] 231 | 232 | 233 | >>> values = [(1,0,0), (None,1,1), (None,0,1), (0,0,0)] 234 | >>> for front in DominanceMatrix(values, dominates).get_pareto_fronts(): 235 | ... print(front) 236 | [(1, 0, 0), (None, 1, 1)] 237 | [(None, 0, 1), (0, 0, 0)] 238 | 239 | """ 240 | def __init__(self, values, dominates): 241 | dimension = len(values) 242 | self.is_dominating = defaultdict(list) 243 | self.dominated_by_counter = defaultdict(int) 244 | self.values = list(values) 245 | 246 | for i in range(dimension): 247 | for j in range(i + 1, dimension): 248 | a = values[i] 249 | b = values[j] 250 | rel = dominates(a, b) 251 | if rel == Domination.GREATER: 252 | self.is_dominating[a].append(b) 253 | self.dominated_by_counter[b] += 1 254 | elif rel == Domination.LESS: 255 | self.is_dominating[b].append(a) 256 | self.dominated_by_counter[a] += 1 257 | 258 | def get_pareto_fronts(self): 259 | while self.values: 260 | current_front = [ v for v in self.values if self.dominated_by_counter[v] == 0] 261 | if len(current_front) == 0: 262 | yield list(self.values) 263 | return 264 | yield current_front 265 | 266 | for v in current_front: 267 | for dominated in self.is_dominating[v]: 268 | self.dominated_by_counter[dominated] -= 1 269 | 270 | self.values.remove(v) 271 | try: 272 | del self.is_dominating[v] 273 | except KeyError: 274 | pass 275 | try: 276 | del self.dominated_by_counter[v] 277 | except KeyError: 278 | pass 279 | 280 | 281 | class Cmp(metaclass=abc.ABCMeta): 282 | @abc.abstractclassmethod 283 | def compare(self, a, b) -> Domination: 284 | raise NotImplementedError 285 | 286 | @abc.abstractclassmethod 287 | def is_pareto(self) -> bool: 288 | raise NotImplementedError 289 | 290 | @abc.abstractclassmethod 291 | def is_group(self) -> bool: 292 | raise NotImplementedError 293 | 294 | @abc.abstractclassmethod 295 | def group(self, a) -> int: 296 | raise NotImplementedError 297 | 298 | class GroupNones(Cmp): 299 | def __init__(self, targets: MaxMinList): 300 | self._targets = targets 301 | @property 302 | def targets(self): 303 | return self._targets 304 | def compare(self, a, b) -> Domination: 305 | raise NotImplementedError 306 | def is_pareto(self) -> bool: 307 | return False 308 | def is_group(self) -> bool: 309 | return True 310 | 311 | def group(self, a) -> int: 312 | """ 313 | GroupNones(MaxMinList(MaxMin.MIN, MaxMin.MIN, MaxMin.MIN)).group((0,None,None)) 314 | -2 315 | """ 316 | noneSum = 0 317 | for d in range(self.targets.dim): 318 | if self.targets.list[d] is MaxMin.SKIP: 319 | continue 320 | if self.targets.list[d] is MaxMin.MAX and a[d] is None: 321 | noneSum += 1 322 | if self.targets.list[d] is MaxMin.MIN and a[d] is None: 323 | noneSum -= 1 324 | return noneSum 325 | 326 | def and_then(self, c: Cmp) -> Cmp: 327 | return ComparisonChain(self, c) 328 | 329 | def as_chain(self): 330 | return ComparisonChain(self) 331 | 332 | 333 | class Comparison(Cmp): 334 | def __init__(self, cmp, targets: MaxMinList): 335 | self._cmp = cmp 336 | self._targets = targets 337 | 338 | def is_pareto(self) -> bool: return True 339 | def is_group(self) -> bool: return False 340 | def group(self, a) -> int: return 1 341 | 342 | @property 343 | def cmp(self): 344 | return self._cmp 345 | 346 | @property 347 | def targets(self): 348 | return self._targets 349 | 350 | def compare(self, a,b) -> Domination: 351 | return dominates(a, b, self.cmp, self.targets) 352 | 353 | def and_then(self, c: Cmp) -> Cmp: 354 | return ComparisonChain(self, c) 355 | 356 | def as_chain(self): 357 | return ComparisonChain(self) 358 | 359 | class ComparisonChain: 360 | def __init__(self, *chain: List[Comparison]): 361 | self._chain = chain 362 | 363 | @property 364 | def chain(self) -> List[Comparison]: 365 | return self._chain 366 | 367 | def compare(self, a, b) -> Domination: 368 | for c in self.chain[:-1]: 369 | if not c.is_pareto(): 370 | continue 371 | result = c.compare(a, b) 372 | if result == Domination.EQUAL: 373 | continue 374 | return result 375 | return self.chain[-1].compare(a, b) 376 | 377 | def and_then(self, c: Cmp) -> Cmp: 378 | return ComparisonChain(self, *self.chain, c) 379 | 380 | 381 | def split_by_pareto(self, values): 382 | """ComparisonChain.split_by_pareto performs the pareto front split fronts. 383 | 384 | Currently this works only for unique rows. You can add id as the last row (and not sort by it) to work around this restriction. 385 | 386 | Here the None means just inferior value: 387 | 388 | >>> values = [(0,None,None), (2,2,2), (0,1,1), (0,0,1), (None,0,1), (0,1,0), (None,1,1), (1,0,0), (0,0,0)] 389 | >>> chain = Comparison(by_value, MaxMinList(MaxMin.MAX, MaxMin.MAX, MaxMin.MAX)).as_chain() 390 | >>> chain.split_by_pareto(values) 391 | [[(2, 2, 2)], [(0, 1, 1), (1, 0, 0)], [(0, 0, 1), (0, 1, 0), (None, 1, 1)], [(None, 0, 1), (0, 0, 0)], [(0, None, None)]] 392 | 393 | Here one extra None means that the whole row is inferior: 394 | 395 | >>> values = [(0,None,None), (2,2,2), (0,1,1), (0,0,1), (None,0,1), (0,1,0), (None,1,1), (1,0,0), (0,0,0), (None, 0, None)] 396 | >>> chain = GroupNones(MaxMinList(MaxMin.MIN, MaxMin.MIN, MaxMin.MIN)).and_then( 397 | ... Comparison(by_value, MaxMinList(MaxMin.MAX, MaxMin.MAX, MaxMin.MAX))) 398 | >>> chain.split_by_pareto(values) 399 | [[(2, 2, 2)], [(0, 1, 1), (1, 0, 0)], [(0, 0, 1), (0, 1, 0)], [(0, 0, 0)], [(None, 1, 1)], [(None, 0, 1)], [(0, None, None), (None, 0, None)]] 400 | 401 | """ 402 | splitted = [values] 403 | for comparison in self.chain: 404 | if comparison.is_pareto(): 405 | new_splitted = [] 406 | while len(splitted) > 0: 407 | group = splitted.pop(0) 408 | new_groups = list(DominanceMatrix(group, comparison.compare).get_pareto_fronts()) 409 | new_splitted.extend(new_groups) 410 | splitted = new_splitted 411 | elif comparison.is_group(): 412 | new_splitted = [] 413 | while len(splitted) > 0: 414 | group = splitted.pop(0) 415 | group_dict = defaultdict(list) 416 | for a in group: 417 | group_dict[comparison.group(a)].append(a) 418 | keys = list(group_dict.keys()) 419 | keys.sort() 420 | keys.reverse() 421 | new_groups = [group_dict[k] for k in keys] 422 | 423 | new_splitted.extend(new_groups) 424 | splitted = new_splitted 425 | return splitted 426 | 427 | if __name__ == "__main__": 428 | import doctest 429 | doctest.testmod() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2020 Pauli Henrikki Rikula 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | """ 10 | 11 | from setuptools import setup 12 | import pypareto 13 | 14 | README = pypareto.__doc__ 15 | 16 | with open('README.md', 'wt') as readme_file: 17 | readme_file.write(README) 18 | 19 | setup( 20 | name='pypareto', 21 | version='0.3.1', 22 | description='Python library for pareto front extraction', 23 | long_description=README, 24 | license="MIT", 25 | author="Pauli Rikula", 26 | url='https://github.com/kummahiih/pypareto', 27 | packages=['pypareto'], 28 | python_requires='~=3.6', 29 | classifiers=[ 30 | 'License :: OSI Approved :: MIT License', 31 | 'Programming Language :: Python :: 3.6'] 32 | ) 33 | --------------------------------------------------------------------------------