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