Coverage for /usr/lib/python3/dist-packages/sympy/polys/polyoptions.py: 58%
441 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""Options manager for :class:`~.Poly` and public API functions. """
3from __future__ import annotations
5__all__ = ["Options"]
7from sympy.core import Basic, sympify
8from sympy.polys.polyerrors import GeneratorsError, OptionError, FlagError
9from sympy.utilities import numbered_symbols, topological_sort, public
10from sympy.utilities.iterables import has_dups, is_sequence
12import sympy.polys
14import re
16class Option:
17 """Base class for all kinds of options. """
19 option: str | None = None
21 is_Flag = False
23 requires: list[str] = []
24 excludes: list[str] = []
26 after: list[str] = []
27 before: list[str] = []
29 @classmethod
30 def default(cls):
31 return None
33 @classmethod
34 def preprocess(cls, option):
35 return None
37 @classmethod
38 def postprocess(cls, options):
39 pass
42class Flag(Option):
43 """Base class for all kinds of flags. """
45 is_Flag = True
48class BooleanOption(Option):
49 """An option that must have a boolean value or equivalent assigned. """
51 @classmethod
52 def preprocess(cls, value):
53 if value in [True, False]:
54 return bool(value)
55 else:
56 raise OptionError("'%s' must have a boolean value assigned, got %s" % (cls.option, value))
59class OptionType(type):
60 """Base type for all options that does registers options. """
62 def __init__(cls, *args, **kwargs):
63 @property
64 def getter(self):
65 try:
66 return self[cls.option]
67 except KeyError:
68 return cls.default()
70 setattr(Options, cls.option, getter)
71 Options.__options__[cls.option] = cls
74@public
75class Options(dict):
76 """
77 Options manager for polynomial manipulation module.
79 Examples
80 ========
82 >>> from sympy.polys.polyoptions import Options
83 >>> from sympy.polys.polyoptions import build_options
85 >>> from sympy.abc import x, y, z
87 >>> Options((x, y, z), {'domain': 'ZZ'})
88 {'auto': False, 'domain': ZZ, 'gens': (x, y, z)}
90 >>> build_options((x, y, z), {'domain': 'ZZ'})
91 {'auto': False, 'domain': ZZ, 'gens': (x, y, z)}
93 **Options**
95 * Expand --- boolean option
96 * Gens --- option
97 * Wrt --- option
98 * Sort --- option
99 * Order --- option
100 * Field --- boolean option
101 * Greedy --- boolean option
102 * Domain --- option
103 * Split --- boolean option
104 * Gaussian --- boolean option
105 * Extension --- option
106 * Modulus --- option
107 * Symmetric --- boolean option
108 * Strict --- boolean option
110 **Flags**
112 * Auto --- boolean flag
113 * Frac --- boolean flag
114 * Formal --- boolean flag
115 * Polys --- boolean flag
116 * Include --- boolean flag
117 * All --- boolean flag
118 * Gen --- flag
119 * Series --- boolean flag
121 """
123 __order__ = None
124 __options__: dict[str, type[Option]] = {}
126 def __init__(self, gens, args, flags=None, strict=False):
127 dict.__init__(self)
129 if gens and args.get('gens', ()):
130 raise OptionError(
131 "both '*gens' and keyword argument 'gens' supplied")
132 elif gens:
133 args = dict(args)
134 args['gens'] = gens
136 defaults = args.pop('defaults', {})
138 def preprocess_options(args):
139 for option, value in args.items():
140 try:
141 cls = self.__options__[option]
142 except KeyError:
143 raise OptionError("'%s' is not a valid option" % option)
145 if issubclass(cls, Flag):
146 if flags is None or option not in flags:
147 if strict:
148 raise OptionError("'%s' flag is not allowed in this context" % option)
150 if value is not None:
151 self[option] = cls.preprocess(value)
153 preprocess_options(args)
155 for key, value in dict(defaults).items():
156 if key in self:
157 del defaults[key]
158 else:
159 for option in self.keys():
160 cls = self.__options__[option]
162 if key in cls.excludes:
163 del defaults[key]
164 break
166 preprocess_options(defaults)
168 for option in self.keys():
169 cls = self.__options__[option]
171 for require_option in cls.requires:
172 if self.get(require_option) is None:
173 raise OptionError("'%s' option is only allowed together with '%s'" % (option, require_option))
175 for exclude_option in cls.excludes:
176 if self.get(exclude_option) is not None:
177 raise OptionError("'%s' option is not allowed together with '%s'" % (option, exclude_option))
179 for option in self.__order__:
180 self.__options__[option].postprocess(self)
182 @classmethod
183 def _init_dependencies_order(cls):
184 """Resolve the order of options' processing. """
185 if cls.__order__ is None:
186 vertices, edges = [], set()
188 for name, option in cls.__options__.items():
189 vertices.append(name)
191 for _name in option.after:
192 edges.add((_name, name))
194 for _name in option.before:
195 edges.add((name, _name))
197 try:
198 cls.__order__ = topological_sort((vertices, list(edges)))
199 except ValueError:
200 raise RuntimeError(
201 "cycle detected in sympy.polys options framework")
203 def clone(self, updates={}):
204 """Clone ``self`` and update specified options. """
205 obj = dict.__new__(self.__class__)
207 for option, value in self.items():
208 obj[option] = value
210 for option, value in updates.items():
211 obj[option] = value
213 return obj
215 def __setattr__(self, attr, value):
216 if attr in self.__options__:
217 self[attr] = value
218 else:
219 super().__setattr__(attr, value)
221 @property
222 def args(self):
223 args = {}
225 for option, value in self.items():
226 if value is not None and option != 'gens':
227 cls = self.__options__[option]
229 if not issubclass(cls, Flag):
230 args[option] = value
232 return args
234 @property
235 def options(self):
236 options = {}
238 for option, cls in self.__options__.items():
239 if not issubclass(cls, Flag):
240 options[option] = getattr(self, option)
242 return options
244 @property
245 def flags(self):
246 flags = {}
248 for option, cls in self.__options__.items():
249 if issubclass(cls, Flag):
250 flags[option] = getattr(self, option)
252 return flags
255class Expand(BooleanOption, metaclass=OptionType):
256 """``expand`` option to polynomial manipulation functions. """
258 option = 'expand'
260 requires: list[str] = []
261 excludes: list[str] = []
263 @classmethod
264 def default(cls):
265 return True
268class Gens(Option, metaclass=OptionType):
269 """``gens`` option to polynomial manipulation functions. """
271 option = 'gens'
273 requires: list[str] = []
274 excludes: list[str] = []
276 @classmethod
277 def default(cls):
278 return ()
280 @classmethod
281 def preprocess(cls, gens):
282 if isinstance(gens, Basic):
283 gens = (gens,)
284 elif len(gens) == 1 and is_sequence(gens[0]):
285 gens = gens[0]
287 if gens == (None,):
288 gens = ()
289 elif has_dups(gens):
290 raise GeneratorsError("duplicated generators: %s" % str(gens))
291 elif any(gen.is_commutative is False for gen in gens):
292 raise GeneratorsError("non-commutative generators: %s" % str(gens))
294 return tuple(gens)
297class Wrt(Option, metaclass=OptionType):
298 """``wrt`` option to polynomial manipulation functions. """
300 option = 'wrt'
302 requires: list[str] = []
303 excludes: list[str] = []
305 _re_split = re.compile(r"\s*,\s*|\s+")
307 @classmethod
308 def preprocess(cls, wrt):
309 if isinstance(wrt, Basic):
310 return [str(wrt)]
311 elif isinstance(wrt, str):
312 wrt = wrt.strip()
313 if wrt.endswith(','):
314 raise OptionError('Bad input: missing parameter.')
315 if not wrt:
316 return []
317 return list(cls._re_split.split(wrt))
318 elif hasattr(wrt, '__getitem__'):
319 return list(map(str, wrt))
320 else:
321 raise OptionError("invalid argument for 'wrt' option")
324class Sort(Option, metaclass=OptionType):
325 """``sort`` option to polynomial manipulation functions. """
327 option = 'sort'
329 requires: list[str] = []
330 excludes: list[str] = []
332 @classmethod
333 def default(cls):
334 return []
336 @classmethod
337 def preprocess(cls, sort):
338 if isinstance(sort, str):
339 return [ gen.strip() for gen in sort.split('>') ]
340 elif hasattr(sort, '__getitem__'):
341 return list(map(str, sort))
342 else:
343 raise OptionError("invalid argument for 'sort' option")
346class Order(Option, metaclass=OptionType):
347 """``order`` option to polynomial manipulation functions. """
349 option = 'order'
351 requires: list[str] = []
352 excludes: list[str] = []
354 @classmethod
355 def default(cls):
356 return sympy.polys.orderings.lex
358 @classmethod
359 def preprocess(cls, order):
360 return sympy.polys.orderings.monomial_key(order)
363class Field(BooleanOption, metaclass=OptionType):
364 """``field`` option to polynomial manipulation functions. """
366 option = 'field'
368 requires: list[str] = []
369 excludes = ['domain', 'split', 'gaussian']
372class Greedy(BooleanOption, metaclass=OptionType):
373 """``greedy`` option to polynomial manipulation functions. """
375 option = 'greedy'
377 requires: list[str] = []
378 excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric']
381class Composite(BooleanOption, metaclass=OptionType):
382 """``composite`` option to polynomial manipulation functions. """
384 option = 'composite'
386 @classmethod
387 def default(cls):
388 return None
390 requires: list[str] = []
391 excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric']
394class Domain(Option, metaclass=OptionType):
395 """``domain`` option to polynomial manipulation functions. """
397 option = 'domain'
399 requires: list[str] = []
400 excludes = ['field', 'greedy', 'split', 'gaussian', 'extension']
402 after = ['gens']
404 _re_realfield = re.compile(r"^(R|RR)(_(\d+))?$")
405 _re_complexfield = re.compile(r"^(C|CC)(_(\d+))?$")
406 _re_finitefield = re.compile(r"^(FF|GF)\((\d+)\)$")
407 _re_polynomial = re.compile(r"^(Z|ZZ|Q|QQ|ZZ_I|QQ_I|R|RR|C|CC)\[(.+)\]$")
408 _re_fraction = re.compile(r"^(Z|ZZ|Q|QQ)\((.+)\)$")
409 _re_algebraic = re.compile(r"^(Q|QQ)\<(.+)\>$")
411 @classmethod
412 def preprocess(cls, domain):
413 if isinstance(domain, sympy.polys.domains.Domain):
414 return domain
415 elif hasattr(domain, 'to_domain'):
416 return domain.to_domain()
417 elif isinstance(domain, str):
418 if domain in ['Z', 'ZZ']:
419 return sympy.polys.domains.ZZ
421 if domain in ['Q', 'QQ']:
422 return sympy.polys.domains.QQ
424 if domain == 'ZZ_I':
425 return sympy.polys.domains.ZZ_I
427 if domain == 'QQ_I':
428 return sympy.polys.domains.QQ_I
430 if domain == 'EX':
431 return sympy.polys.domains.EX
433 r = cls._re_realfield.match(domain)
435 if r is not None:
436 _, _, prec = r.groups()
438 if prec is None:
439 return sympy.polys.domains.RR
440 else:
441 return sympy.polys.domains.RealField(int(prec))
443 r = cls._re_complexfield.match(domain)
445 if r is not None:
446 _, _, prec = r.groups()
448 if prec is None:
449 return sympy.polys.domains.CC
450 else:
451 return sympy.polys.domains.ComplexField(int(prec))
453 r = cls._re_finitefield.match(domain)
455 if r is not None:
456 return sympy.polys.domains.FF(int(r.groups()[1]))
458 r = cls._re_polynomial.match(domain)
460 if r is not None:
461 ground, gens = r.groups()
463 gens = list(map(sympify, gens.split(',')))
465 if ground in ['Z', 'ZZ']:
466 return sympy.polys.domains.ZZ.poly_ring(*gens)
467 elif ground in ['Q', 'QQ']:
468 return sympy.polys.domains.QQ.poly_ring(*gens)
469 elif ground in ['R', 'RR']:
470 return sympy.polys.domains.RR.poly_ring(*gens)
471 elif ground == 'ZZ_I':
472 return sympy.polys.domains.ZZ_I.poly_ring(*gens)
473 elif ground == 'QQ_I':
474 return sympy.polys.domains.QQ_I.poly_ring(*gens)
475 else:
476 return sympy.polys.domains.CC.poly_ring(*gens)
478 r = cls._re_fraction.match(domain)
480 if r is not None:
481 ground, gens = r.groups()
483 gens = list(map(sympify, gens.split(',')))
485 if ground in ['Z', 'ZZ']:
486 return sympy.polys.domains.ZZ.frac_field(*gens)
487 else:
488 return sympy.polys.domains.QQ.frac_field(*gens)
490 r = cls._re_algebraic.match(domain)
492 if r is not None:
493 gens = list(map(sympify, r.groups()[1].split(',')))
494 return sympy.polys.domains.QQ.algebraic_field(*gens)
496 raise OptionError('expected a valid domain specification, got %s' % domain)
498 @classmethod
499 def postprocess(cls, options):
500 if 'gens' in options and 'domain' in options and options['domain'].is_Composite and \
501 (set(options['domain'].symbols) & set(options['gens'])):
502 raise GeneratorsError(
503 "ground domain and generators interfere together")
504 elif ('gens' not in options or not options['gens']) and \
505 'domain' in options and options['domain'] == sympy.polys.domains.EX:
506 raise GeneratorsError("you have to provide generators because EX domain was requested")
509class Split(BooleanOption, metaclass=OptionType):
510 """``split`` option to polynomial manipulation functions. """
512 option = 'split'
514 requires: list[str] = []
515 excludes = ['field', 'greedy', 'domain', 'gaussian', 'extension',
516 'modulus', 'symmetric']
518 @classmethod
519 def postprocess(cls, options):
520 if 'split' in options:
521 raise NotImplementedError("'split' option is not implemented yet")
524class Gaussian(BooleanOption, metaclass=OptionType):
525 """``gaussian`` option to polynomial manipulation functions. """
527 option = 'gaussian'
529 requires: list[str] = []
530 excludes = ['field', 'greedy', 'domain', 'split', 'extension',
531 'modulus', 'symmetric']
533 @classmethod
534 def postprocess(cls, options):
535 if 'gaussian' in options and options['gaussian'] is True:
536 options['domain'] = sympy.polys.domains.QQ_I
537 Extension.postprocess(options)
540class Extension(Option, metaclass=OptionType):
541 """``extension`` option to polynomial manipulation functions. """
543 option = 'extension'
545 requires: list[str] = []
546 excludes = ['greedy', 'domain', 'split', 'gaussian', 'modulus',
547 'symmetric']
549 @classmethod
550 def preprocess(cls, extension):
551 if extension == 1:
552 return bool(extension)
553 elif extension == 0:
554 raise OptionError("'False' is an invalid argument for 'extension'")
555 else:
556 if not hasattr(extension, '__iter__'):
557 extension = {extension}
558 else:
559 if not extension:
560 extension = None
561 else:
562 extension = set(extension)
564 return extension
566 @classmethod
567 def postprocess(cls, options):
568 if 'extension' in options and options['extension'] is not True:
569 options['domain'] = sympy.polys.domains.QQ.algebraic_field(
570 *options['extension'])
573class Modulus(Option, metaclass=OptionType):
574 """``modulus`` option to polynomial manipulation functions. """
576 option = 'modulus'
578 requires: list[str] = []
579 excludes = ['greedy', 'split', 'domain', 'gaussian', 'extension']
581 @classmethod
582 def preprocess(cls, modulus):
583 modulus = sympify(modulus)
585 if modulus.is_Integer and modulus > 0:
586 return int(modulus)
587 else:
588 raise OptionError(
589 "'modulus' must a positive integer, got %s" % modulus)
591 @classmethod
592 def postprocess(cls, options):
593 if 'modulus' in options:
594 modulus = options['modulus']
595 symmetric = options.get('symmetric', True)
596 options['domain'] = sympy.polys.domains.FF(modulus, symmetric)
599class Symmetric(BooleanOption, metaclass=OptionType):
600 """``symmetric`` option to polynomial manipulation functions. """
602 option = 'symmetric'
604 requires = ['modulus']
605 excludes = ['greedy', 'domain', 'split', 'gaussian', 'extension']
608class Strict(BooleanOption, metaclass=OptionType):
609 """``strict`` option to polynomial manipulation functions. """
611 option = 'strict'
613 @classmethod
614 def default(cls):
615 return True
618class Auto(BooleanOption, Flag, metaclass=OptionType):
619 """``auto`` flag to polynomial manipulation functions. """
621 option = 'auto'
623 after = ['field', 'domain', 'extension', 'gaussian']
625 @classmethod
626 def default(cls):
627 return True
629 @classmethod
630 def postprocess(cls, options):
631 if ('domain' in options or 'field' in options) and 'auto' not in options:
632 options['auto'] = False
635class Frac(BooleanOption, Flag, metaclass=OptionType):
636 """``auto`` option to polynomial manipulation functions. """
638 option = 'frac'
640 @classmethod
641 def default(cls):
642 return False
645class Formal(BooleanOption, Flag, metaclass=OptionType):
646 """``formal`` flag to polynomial manipulation functions. """
648 option = 'formal'
650 @classmethod
651 def default(cls):
652 return False
655class Polys(BooleanOption, Flag, metaclass=OptionType):
656 """``polys`` flag to polynomial manipulation functions. """
658 option = 'polys'
661class Include(BooleanOption, Flag, metaclass=OptionType):
662 """``include`` flag to polynomial manipulation functions. """
664 option = 'include'
666 @classmethod
667 def default(cls):
668 return False
671class All(BooleanOption, Flag, metaclass=OptionType):
672 """``all`` flag to polynomial manipulation functions. """
674 option = 'all'
676 @classmethod
677 def default(cls):
678 return False
681class Gen(Flag, metaclass=OptionType):
682 """``gen`` flag to polynomial manipulation functions. """
684 option = 'gen'
686 @classmethod
687 def default(cls):
688 return 0
690 @classmethod
691 def preprocess(cls, gen):
692 if isinstance(gen, (Basic, int)):
693 return gen
694 else:
695 raise OptionError("invalid argument for 'gen' option")
698class Series(BooleanOption, Flag, metaclass=OptionType):
699 """``series`` flag to polynomial manipulation functions. """
701 option = 'series'
703 @classmethod
704 def default(cls):
705 return False
708class Symbols(Flag, metaclass=OptionType):
709 """``symbols`` flag to polynomial manipulation functions. """
711 option = 'symbols'
713 @classmethod
714 def default(cls):
715 return numbered_symbols('s', start=1)
717 @classmethod
718 def preprocess(cls, symbols):
719 if hasattr(symbols, '__iter__'):
720 return iter(symbols)
721 else:
722 raise OptionError("expected an iterator or iterable container, got %s" % symbols)
725class Method(Flag, metaclass=OptionType):
726 """``method`` flag to polynomial manipulation functions. """
728 option = 'method'
730 @classmethod
731 def preprocess(cls, method):
732 if isinstance(method, str):
733 return method.lower()
734 else:
735 raise OptionError("expected a string, got %s" % method)
738def build_options(gens, args=None):
739 """Construct options from keyword arguments or ... options. """
740 if args is None:
741 gens, args = (), gens
743 if len(args) != 1 or 'opt' not in args or gens:
744 return Options(gens, args)
745 else:
746 return args['opt']
749def allowed_flags(args, flags):
750 """
751 Allow specified flags to be used in the given context.
753 Examples
754 ========
756 >>> from sympy.polys.polyoptions import allowed_flags
757 >>> from sympy.polys.domains import ZZ
759 >>> allowed_flags({'domain': ZZ}, [])
761 >>> allowed_flags({'domain': ZZ, 'frac': True}, [])
762 Traceback (most recent call last):
763 ...
764 FlagError: 'frac' flag is not allowed in this context
766 >>> allowed_flags({'domain': ZZ, 'frac': True}, ['frac'])
768 """
769 flags = set(flags)
771 for arg in args.keys():
772 try:
773 if Options.__options__[arg].is_Flag and arg not in flags:
774 raise FlagError(
775 "'%s' flag is not allowed in this context" % arg)
776 except KeyError:
777 raise OptionError("'%s' is not a valid option" % arg)
780def set_defaults(options, **defaults):
781 """Update options with default values. """
782 if 'defaults' not in options:
783 options = dict(options)
784 options['defaults'] = defaults
786 return options
788Options._init_dependencies_order()