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