1"""Options manager for :class:`~.Poly` and public API functions. """ 2 3 4__all__ = ["Options"] 5 6from typing import Dict, Type 7from typing import List, Optional 8 9from sympy.core import Basic, sympify 10from sympy.polys.polyerrors import GeneratorsError, OptionError, FlagError 11from sympy.utilities import numbered_symbols, topological_sort, public 12from sympy.utilities.iterables import has_dups 13from sympy.core.compatibility import is_sequence 14 15import sympy.polys 16 17import re 18 19class Option: 20 """Base class for all kinds of options. """ 21 22 option = None # type: Optional[str] 23 24 is_Flag = False 25 26 requires = [] # type: List[str] 27 excludes = [] # type: List[str] 28 29 after = [] # type: List[str] 30 before = [] # type: List[str] 31 32 @classmethod 33 def default(cls): 34 return None 35 36 @classmethod 37 def preprocess(cls, option): 38 return None 39 40 @classmethod 41 def postprocess(cls, options): 42 pass 43 44 45class Flag(Option): 46 """Base class for all kinds of flags. """ 47 48 is_Flag = True 49 50 51class BooleanOption(Option): 52 """An option that must have a boolean value or equivalent assigned. """ 53 54 @classmethod 55 def preprocess(cls, value): 56 if value in [True, False]: 57 return bool(value) 58 else: 59 raise OptionError("'%s' must have a boolean value assigned, got %s" % (cls.option, value)) 60 61 62class OptionType(type): 63 """Base type for all options that does registers options. """ 64 65 def __init__(cls, *args, **kwargs): 66 @property 67 def getter(self): 68 try: 69 return self[cls.option] 70 except KeyError: 71 return cls.default() 72 73 setattr(Options, cls.option, getter) 74 Options.__options__[cls.option] = cls 75 76 77@public 78class Options(dict): 79 """ 80 Options manager for polynomial manipulation module. 81 82 Examples 83 ======== 84 85 >>> from sympy.polys.polyoptions import Options 86 >>> from sympy.polys.polyoptions import build_options 87 88 >>> from sympy.abc import x, y, z 89 90 >>> Options((x, y, z), {'domain': 'ZZ'}) 91 {'auto': False, 'domain': ZZ, 'gens': (x, y, z)} 92 93 >>> build_options((x, y, z), {'domain': 'ZZ'}) 94 {'auto': False, 'domain': ZZ, 'gens': (x, y, z)} 95 96 **Options** 97 98 * Expand --- boolean option 99 * Gens --- option 100 * Wrt --- option 101 * Sort --- option 102 * Order --- option 103 * Field --- boolean option 104 * Greedy --- boolean option 105 * Domain --- option 106 * Split --- boolean option 107 * Gaussian --- boolean option 108 * Extension --- option 109 * Modulus --- option 110 * Symmetric --- boolean option 111 * Strict --- boolean option 112 113 **Flags** 114 115 * Auto --- boolean flag 116 * Frac --- boolean flag 117 * Formal --- boolean flag 118 * Polys --- boolean flag 119 * Include --- boolean flag 120 * All --- boolean flag 121 * Gen --- flag 122 * Series --- boolean flag 123 124 """ 125 126 __order__ = None 127 __options__ = {} # type: Dict[str, Type[Option]] 128 129 def __init__(self, gens, args, flags=None, strict=False): 130 dict.__init__(self) 131 132 if gens and args.get('gens', ()): 133 raise OptionError( 134 "both '*gens' and keyword argument 'gens' supplied") 135 elif gens: 136 args = dict(args) 137 args['gens'] = gens 138 139 defaults = args.pop('defaults', {}) 140 141 def preprocess_options(args): 142 for option, value in args.items(): 143 try: 144 cls = self.__options__[option] 145 except KeyError: 146 raise OptionError("'%s' is not a valid option" % option) 147 148 if issubclass(cls, Flag): 149 if flags is None or option not in flags: 150 if strict: 151 raise OptionError("'%s' flag is not allowed in this context" % option) 152 153 if value is not None: 154 self[option] = cls.preprocess(value) 155 156 preprocess_options(args) 157 158 for key, value in dict(defaults).items(): 159 if key in self: 160 del defaults[key] 161 else: 162 for option in self.keys(): 163 cls = self.__options__[option] 164 165 if key in cls.excludes: 166 del defaults[key] 167 break 168 169 preprocess_options(defaults) 170 171 for option in self.keys(): 172 cls = self.__options__[option] 173 174 for require_option in cls.requires: 175 if self.get(require_option) is None: 176 raise OptionError("'%s' option is only allowed together with '%s'" % (option, require_option)) 177 178 for exclude_option in cls.excludes: 179 if self.get(exclude_option) is not None: 180 raise OptionError("'%s' option is not allowed together with '%s'" % (option, exclude_option)) 181 182 for option in self.__order__: 183 self.__options__[option].postprocess(self) 184 185 @classmethod 186 def _init_dependencies_order(cls): 187 """Resolve the order of options' processing. """ 188 if cls.__order__ is None: 189 vertices, edges = [], set() 190 191 for name, option in cls.__options__.items(): 192 vertices.append(name) 193 194 for _name in option.after: 195 edges.add((_name, name)) 196 197 for _name in option.before: 198 edges.add((name, _name)) 199 200 try: 201 cls.__order__ = topological_sort((vertices, list(edges))) 202 except ValueError: 203 raise RuntimeError( 204 "cycle detected in sympy.polys options framework") 205 206 def clone(self, updates={}): 207 """Clone ``self`` and update specified options. """ 208 obj = dict.__new__(self.__class__) 209 210 for option, value in self.items(): 211 obj[option] = value 212 213 for option, value in updates.items(): 214 obj[option] = value 215 216 return obj 217 218 def __setattr__(self, attr, value): 219 if attr in self.__options__: 220 self[attr] = value 221 else: 222 super().__setattr__(attr, value) 223 224 @property 225 def args(self): 226 args = {} 227 228 for option, value in self.items(): 229 if value is not None and option != 'gens': 230 cls = self.__options__[option] 231 232 if not issubclass(cls, Flag): 233 args[option] = value 234 235 return args 236 237 @property 238 def options(self): 239 options = {} 240 241 for option, cls in self.__options__.items(): 242 if not issubclass(cls, Flag): 243 options[option] = getattr(self, option) 244 245 return options 246 247 @property 248 def flags(self): 249 flags = {} 250 251 for option, cls in self.__options__.items(): 252 if issubclass(cls, Flag): 253 flags[option] = getattr(self, option) 254 255 return flags 256 257 258class Expand(BooleanOption, metaclass=OptionType): 259 """``expand`` option to polynomial manipulation functions. """ 260 261 option = 'expand' 262 263 requires = [] # type: List[str] 264 excludes = [] # type: List[str] 265 266 @classmethod 267 def default(cls): 268 return True 269 270 271class Gens(Option, metaclass=OptionType): 272 """``gens`` option to polynomial manipulation functions. """ 273 274 option = 'gens' 275 276 requires = [] # type: List[str] 277 excludes = [] # type: List[str] 278 279 @classmethod 280 def default(cls): 281 return () 282 283 @classmethod 284 def preprocess(cls, gens): 285 if isinstance(gens, Basic): 286 gens = (gens,) 287 elif len(gens) == 1 and is_sequence(gens[0]): 288 gens = gens[0] 289 290 if gens == (None,): 291 gens = () 292 elif has_dups(gens): 293 raise GeneratorsError("duplicated generators: %s" % str(gens)) 294 elif any(gen.is_commutative is False for gen in gens): 295 raise GeneratorsError("non-commutative generators: %s" % str(gens)) 296 297 return tuple(gens) 298 299 300class Wrt(Option, metaclass=OptionType): 301 """``wrt`` option to polynomial manipulation functions. """ 302 303 option = 'wrt' 304 305 requires = [] # type: List[str] 306 excludes = [] # type: List[str] 307 308 _re_split = re.compile(r"\s*,\s*|\s+") 309 310 @classmethod 311 def preprocess(cls, wrt): 312 if isinstance(wrt, Basic): 313 return [str(wrt)] 314 elif isinstance(wrt, str): 315 wrt = wrt.strip() 316 if wrt.endswith(','): 317 raise OptionError('Bad input: missing parameter.') 318 if not wrt: 319 return [] 320 return [ gen for gen in cls._re_split.split(wrt) ] 321 elif hasattr(wrt, '__getitem__'): 322 return list(map(str, wrt)) 323 else: 324 raise OptionError("invalid argument for 'wrt' option") 325 326 327class Sort(Option, metaclass=OptionType): 328 """``sort`` option to polynomial manipulation functions. """ 329 330 option = 'sort' 331 332 requires = [] # type: List[str] 333 excludes = [] # type: List[str] 334 335 @classmethod 336 def default(cls): 337 return [] 338 339 @classmethod 340 def preprocess(cls, sort): 341 if isinstance(sort, str): 342 return [ gen.strip() for gen in sort.split('>') ] 343 elif hasattr(sort, '__getitem__'): 344 return list(map(str, sort)) 345 else: 346 raise OptionError("invalid argument for 'sort' option") 347 348 349class Order(Option, metaclass=OptionType): 350 """``order`` option to polynomial manipulation functions. """ 351 352 option = 'order' 353 354 requires = [] # type: List[str] 355 excludes = [] # type: List[str] 356 357 @classmethod 358 def default(cls): 359 return sympy.polys.orderings.lex 360 361 @classmethod 362 def preprocess(cls, order): 363 return sympy.polys.orderings.monomial_key(order) 364 365 366class Field(BooleanOption, metaclass=OptionType): 367 """``field`` option to polynomial manipulation functions. """ 368 369 option = 'field' 370 371 requires = [] # type: List[str] 372 excludes = ['domain', 'split', 'gaussian'] 373 374 375class Greedy(BooleanOption, metaclass=OptionType): 376 """``greedy`` option to polynomial manipulation functions. """ 377 378 option = 'greedy' 379 380 requires = [] # type: List[str] 381 excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric'] 382 383 384class Composite(BooleanOption, metaclass=OptionType): 385 """``composite`` option to polynomial manipulation functions. """ 386 387 option = 'composite' 388 389 @classmethod 390 def default(cls): 391 return None 392 393 requires = [] # type: List[str] 394 excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric'] 395 396 397class Domain(Option, metaclass=OptionType): 398 """``domain`` option to polynomial manipulation functions. """ 399 400 option = 'domain' 401 402 requires = [] # type: List[str] 403 excludes = ['field', 'greedy', 'split', 'gaussian', 'extension'] 404 405 after = ['gens'] 406 407 _re_realfield = re.compile(r"^(R|RR)(_(\d+))?$") 408 _re_complexfield = re.compile(r"^(C|CC)(_(\d+))?$") 409 _re_finitefield = re.compile(r"^(FF|GF)\((\d+)\)$") 410 _re_polynomial = re.compile(r"^(Z|ZZ|Q|QQ|ZZ_I|QQ_I|R|RR|C|CC)\[(.+)\]$") 411 _re_fraction = re.compile(r"^(Z|ZZ|Q|QQ)\((.+)\)$") 412 _re_algebraic = re.compile(r"^(Q|QQ)\<(.+)\>$") 413 414 @classmethod 415 def preprocess(cls, domain): 416 if isinstance(domain, sympy.polys.domains.Domain): 417 return domain 418 elif hasattr(domain, 'to_domain'): 419 return domain.to_domain() 420 elif isinstance(domain, str): 421 if domain in ['Z', 'ZZ']: 422 return sympy.polys.domains.ZZ 423 424 if domain in ['Q', 'QQ']: 425 return sympy.polys.domains.QQ 426 427 if domain == 'ZZ_I': 428 return sympy.polys.domains.ZZ_I 429 430 if domain == 'QQ_I': 431 return sympy.polys.domains.QQ_I 432 433 if domain == 'EX': 434 return sympy.polys.domains.EX 435 436 r = cls._re_realfield.match(domain) 437 438 if r is not None: 439 _, _, prec = r.groups() 440 441 if prec is None: 442 return sympy.polys.domains.RR 443 else: 444 return sympy.polys.domains.RealField(int(prec)) 445 446 r = cls._re_complexfield.match(domain) 447 448 if r is not None: 449 _, _, prec = r.groups() 450 451 if prec is None: 452 return sympy.polys.domains.CC 453 else: 454 return sympy.polys.domains.ComplexField(int(prec)) 455 456 r = cls._re_finitefield.match(domain) 457 458 if r is not None: 459 return sympy.polys.domains.FF(int(r.groups()[1])) 460 461 r = cls._re_polynomial.match(domain) 462 463 if r is not None: 464 ground, gens = r.groups() 465 466 gens = list(map(sympify, gens.split(','))) 467 468 if ground in ['Z', 'ZZ']: 469 return sympy.polys.domains.ZZ.poly_ring(*gens) 470 elif ground in ['Q', 'QQ']: 471 return sympy.polys.domains.QQ.poly_ring(*gens) 472 elif ground in ['R', 'RR']: 473 return sympy.polys.domains.RR.poly_ring(*gens) 474 elif ground == 'ZZ_I': 475 return sympy.polys.domains.ZZ_I.poly_ring(*gens) 476 elif ground == 'QQ_I': 477 return sympy.polys.domains.QQ_I.poly_ring(*gens) 478 else: 479 return sympy.polys.domains.CC.poly_ring(*gens) 480 481 r = cls._re_fraction.match(domain) 482 483 if r is not None: 484 ground, gens = r.groups() 485 486 gens = list(map(sympify, gens.split(','))) 487 488 if ground in ['Z', 'ZZ']: 489 return sympy.polys.domains.ZZ.frac_field(*gens) 490 else: 491 return sympy.polys.domains.QQ.frac_field(*gens) 492 493 r = cls._re_algebraic.match(domain) 494 495 if r is not None: 496 gens = list(map(sympify, r.groups()[1].split(','))) 497 return sympy.polys.domains.QQ.algebraic_field(*gens) 498 499 raise OptionError('expected a valid domain specification, got %s' % domain) 500 501 @classmethod 502 def postprocess(cls, options): 503 if 'gens' in options and 'domain' in options and options['domain'].is_Composite and \ 504 (set(options['domain'].symbols) & set(options['gens'])): 505 raise GeneratorsError( 506 "ground domain and generators interfere together") 507 elif ('gens' not in options or not options['gens']) and \ 508 'domain' in options and options['domain'] == sympy.polys.domains.EX: 509 raise GeneratorsError("you have to provide generators because EX domain was requested") 510 511 512class Split(BooleanOption, metaclass=OptionType): 513 """``split`` option to polynomial manipulation functions. """ 514 515 option = 'split' 516 517 requires = [] # type: List[str] 518 excludes = ['field', 'greedy', 'domain', 'gaussian', 'extension', 519 'modulus', 'symmetric'] 520 521 @classmethod 522 def postprocess(cls, options): 523 if 'split' in options: 524 raise NotImplementedError("'split' option is not implemented yet") 525 526 527class Gaussian(BooleanOption, metaclass=OptionType): 528 """``gaussian`` option to polynomial manipulation functions. """ 529 530 option = 'gaussian' 531 532 requires = [] # type: List[str] 533 excludes = ['field', 'greedy', 'domain', 'split', 'extension', 534 'modulus', 'symmetric'] 535 536 @classmethod 537 def postprocess(cls, options): 538 if 'gaussian' in options and options['gaussian'] is True: 539 options['domain'] = sympy.polys.domains.QQ_I 540 Extension.postprocess(options) 541 542 543class Extension(Option, metaclass=OptionType): 544 """``extension`` option to polynomial manipulation functions. """ 545 546 option = 'extension' 547 548 requires = [] # type: List[str] 549 excludes = ['greedy', 'domain', 'split', 'gaussian', 'modulus', 550 'symmetric'] 551 552 @classmethod 553 def preprocess(cls, extension): 554 if extension == 1: 555 return bool(extension) 556 elif extension == 0: 557 raise OptionError("'False' is an invalid argument for 'extension'") 558 else: 559 if not hasattr(extension, '__iter__'): 560 extension = {extension} 561 else: 562 if not extension: 563 extension = None 564 else: 565 extension = set(extension) 566 567 return extension 568 569 @classmethod 570 def postprocess(cls, options): 571 if 'extension' in options and options['extension'] is not True: 572 options['domain'] = sympy.polys.domains.QQ.algebraic_field( 573 *options['extension']) 574 575 576class Modulus(Option, metaclass=OptionType): 577 """``modulus`` option to polynomial manipulation functions. """ 578 579 option = 'modulus' 580 581 requires = [] # type: List[str] 582 excludes = ['greedy', 'split', 'domain', 'gaussian', 'extension'] 583 584 @classmethod 585 def preprocess(cls, modulus): 586 modulus = sympify(modulus) 587 588 if modulus.is_Integer and modulus > 0: 589 return int(modulus) 590 else: 591 raise OptionError( 592 "'modulus' must a positive integer, got %s" % modulus) 593 594 @classmethod 595 def postprocess(cls, options): 596 if 'modulus' in options: 597 modulus = options['modulus'] 598 symmetric = options.get('symmetric', True) 599 options['domain'] = sympy.polys.domains.FF(modulus, symmetric) 600 601 602class Symmetric(BooleanOption, metaclass=OptionType): 603 """``symmetric`` option to polynomial manipulation functions. """ 604 605 option = 'symmetric' 606 607 requires = ['modulus'] 608 excludes = ['greedy', 'domain', 'split', 'gaussian', 'extension'] 609 610 611class Strict(BooleanOption, metaclass=OptionType): 612 """``strict`` option to polynomial manipulation functions. """ 613 614 option = 'strict' 615 616 @classmethod 617 def default(cls): 618 return True 619 620 621class Auto(BooleanOption, Flag, metaclass=OptionType): 622 """``auto`` flag to polynomial manipulation functions. """ 623 624 option = 'auto' 625 626 after = ['field', 'domain', 'extension', 'gaussian'] 627 628 @classmethod 629 def default(cls): 630 return True 631 632 @classmethod 633 def postprocess(cls, options): 634 if ('domain' in options or 'field' in options) and 'auto' not in options: 635 options['auto'] = False 636 637 638class Frac(BooleanOption, Flag, metaclass=OptionType): 639 """``auto`` option to polynomial manipulation functions. """ 640 641 option = 'frac' 642 643 @classmethod 644 def default(cls): 645 return False 646 647 648class Formal(BooleanOption, Flag, metaclass=OptionType): 649 """``formal`` flag to polynomial manipulation functions. """ 650 651 option = 'formal' 652 653 @classmethod 654 def default(cls): 655 return False 656 657 658class Polys(BooleanOption, Flag, metaclass=OptionType): 659 """``polys`` flag to polynomial manipulation functions. """ 660 661 option = 'polys' 662 663 664class Include(BooleanOption, Flag, metaclass=OptionType): 665 """``include`` flag to polynomial manipulation functions. """ 666 667 option = 'include' 668 669 @classmethod 670 def default(cls): 671 return False 672 673 674class All(BooleanOption, Flag, metaclass=OptionType): 675 """``all`` flag to polynomial manipulation functions. """ 676 677 option = 'all' 678 679 @classmethod 680 def default(cls): 681 return False 682 683 684class Gen(Flag, metaclass=OptionType): 685 """``gen`` flag to polynomial manipulation functions. """ 686 687 option = 'gen' 688 689 @classmethod 690 def default(cls): 691 return 0 692 693 @classmethod 694 def preprocess(cls, gen): 695 if isinstance(gen, (Basic, int)): 696 return gen 697 else: 698 raise OptionError("invalid argument for 'gen' option") 699 700 701class Series(BooleanOption, Flag, metaclass=OptionType): 702 """``series`` flag to polynomial manipulation functions. """ 703 704 option = 'series' 705 706 @classmethod 707 def default(cls): 708 return False 709 710 711class Symbols(Flag, metaclass=OptionType): 712 """``symbols`` flag to polynomial manipulation functions. """ 713 714 option = 'symbols' 715 716 @classmethod 717 def default(cls): 718 return numbered_symbols('s', start=1) 719 720 @classmethod 721 def preprocess(cls, symbols): 722 if hasattr(symbols, '__iter__'): 723 return iter(symbols) 724 else: 725 raise OptionError("expected an iterator or iterable container, got %s" % symbols) 726 727 728class Method(Flag, metaclass=OptionType): 729 """``method`` flag to polynomial manipulation functions. """ 730 731 option = 'method' 732 733 @classmethod 734 def preprocess(cls, method): 735 if isinstance(method, str): 736 return method.lower() 737 else: 738 raise OptionError("expected a string, got %s" % method) 739 740 741def build_options(gens, args=None): 742 """Construct options from keyword arguments or ... options. """ 743 if args is None: 744 gens, args = (), gens 745 746 if len(args) != 1 or 'opt' not in args or gens: 747 return Options(gens, args) 748 else: 749 return args['opt'] 750 751 752def allowed_flags(args, flags): 753 """ 754 Allow specified flags to be used in the given context. 755 756 Examples 757 ======== 758 759 >>> from sympy.polys.polyoptions import allowed_flags 760 >>> from sympy.polys.domains import ZZ 761 762 >>> allowed_flags({'domain': ZZ}, []) 763 764 >>> allowed_flags({'domain': ZZ, 'frac': True}, []) 765 Traceback (most recent call last): 766 ... 767 FlagError: 'frac' flag is not allowed in this context 768 769 >>> allowed_flags({'domain': ZZ, 'frac': True}, ['frac']) 770 771 """ 772 flags = set(flags) 773 774 for arg in args.keys(): 775 try: 776 if Options.__options__[arg].is_Flag and not arg in flags: 777 raise FlagError( 778 "'%s' flag is not allowed in this context" % arg) 779 except KeyError: 780 raise OptionError("'%s' is not a valid option" % arg) 781 782 783def set_defaults(options, **defaults): 784 """Update options with default values. """ 785 if 'defaults' not in options: 786 options = dict(options) 787 options['defaults'] = defaults 788 789 return options 790 791Options._init_dependencies_order() 792