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