"""Options manager for :class:`~.Poly` and public API functions. """ from __future__ import annotations __all__ = ["Options"] from sympy.core import Basic, sympify from sympy.polys.polyerrors import GeneratorsError, OptionError, FlagError from sympy.utilities import numbered_symbols, topological_sort, public from sympy.utilities.iterables import has_dups, is_sequence import sympy.polys import re class Option: """Base class for all kinds of options. """ option: str | None = None is_Flag = False requires: list[str] = [] excludes: list[str] = [] after: list[str] = [] before: list[str] = [] @classmethod def default(cls): return None @classmethod def preprocess(cls, option): return None @classmethod def postprocess(cls, options): pass class Flag(Option): """Base class for all kinds of flags. """ is_Flag = True class BooleanOption(Option): """An option that must have a boolean value or equivalent assigned. """ @classmethod def preprocess(cls, value): if value in [True, False]: return bool(value) else: raise OptionError("'%s' must have a boolean value assigned, got %s" % (cls.option, value)) class OptionType(type): """Base type for all options that does registers options. """ def __init__(cls, *args, **kwargs): @property def getter(self): try: return self[cls.option] except KeyError: return cls.default() setattr(Options, cls.option, getter) Options.__options__[cls.option] = cls @public class Options(dict): """ Options manager for polynomial manipulation module. Examples ======== >>> from sympy.polys.polyoptions import Options >>> from sympy.polys.polyoptions import build_options >>> from sympy.abc import x, y, z >>> Options((x, y, z), {'domain': 'ZZ'}) {'auto': False, 'domain': ZZ, 'gens': (x, y, z)} >>> build_options((x, y, z), {'domain': 'ZZ'}) {'auto': False, 'domain': ZZ, 'gens': (x, y, z)} **Options** * Expand --- boolean option * Gens --- option * Wrt --- option * Sort --- option * Order --- option * Field --- boolean option * Greedy --- boolean option * Domain --- option * Split --- boolean option * Gaussian --- boolean option * Extension --- option * Modulus --- option * Symmetric --- boolean option * Strict --- boolean option **Flags** * Auto --- boolean flag * Frac --- boolean flag * Formal --- boolean flag * Polys --- boolean flag * Include --- boolean flag * All --- boolean flag * Gen --- flag * Series --- boolean flag """ __order__ = None __options__: dict[str, type[Option]] = {} def __init__(self, gens, args, flags=None, strict=False): dict.__init__(self) if gens and args.get('gens', ()): raise OptionError( "both '*gens' and keyword argument 'gens' supplied") elif gens: args = dict(args) args['gens'] = gens defaults = args.pop('defaults', {}) def preprocess_options(args): for option, value in args.items(): try: cls = self.__options__[option] except KeyError: raise OptionError("'%s' is not a valid option" % option) if issubclass(cls, Flag): if flags is None or option not in flags: if strict: raise OptionError("'%s' flag is not allowed in this context" % option) if value is not None: self[option] = cls.preprocess(value) preprocess_options(args) for key in dict(defaults): if key in self: del defaults[key] else: for option in self.keys(): cls = self.__options__[option] if key in cls.excludes: del defaults[key] break preprocess_options(defaults) for option in self.keys(): cls = self.__options__[option] for require_option in cls.requires: if self.get(require_option) is None: raise OptionError("'%s' option is only allowed together with '%s'" % (option, require_option)) for exclude_option in cls.excludes: if self.get(exclude_option) is not None: raise OptionError("'%s' option is not allowed together with '%s'" % (option, exclude_option)) for option in self.__order__: self.__options__[option].postprocess(self) @classmethod def _init_dependencies_order(cls): """Resolve the order of options' processing. """ if cls.__order__ is None: vertices, edges = [], set() for name, option in cls.__options__.items(): vertices.append(name) edges.update((_name, name) for _name in option.after) edges.update((name, _name) for _name in option.before) try: cls.__order__ = topological_sort((vertices, list(edges))) except ValueError: raise RuntimeError( "cycle detected in sympy.polys options framework") def clone(self, updates={}): """Clone ``self`` and update specified options. """ obj = dict.__new__(self.__class__) for option, value in self.items(): obj[option] = value for option, value in updates.items(): obj[option] = value return obj def __setattr__(self, attr, value): if attr in self.__options__: self[attr] = value else: super().__setattr__(attr, value) @property def args(self): args = {} for option, value in self.items(): if value is not None and option != 'gens': cls = self.__options__[option] if not issubclass(cls, Flag): args[option] = value return args @property def options(self): options = {} for option, cls in self.__options__.items(): if not issubclass(cls, Flag): options[option] = getattr(self, option) return options @property def flags(self): flags = {} for option, cls in self.__options__.items(): if issubclass(cls, Flag): flags[option] = getattr(self, option) return flags class Expand(BooleanOption, metaclass=OptionType): """``expand`` option to polynomial manipulation functions. """ option = 'expand' requires: list[str] = [] excludes: list[str] = [] @classmethod def default(cls): return True class Gens(Option, metaclass=OptionType): """``gens`` option to polynomial manipulation functions. """ option = 'gens' requires: list[str] = [] excludes: list[str] = [] @classmethod def default(cls): return () @classmethod def preprocess(cls, gens): if isinstance(gens, Basic): gens = (gens,) elif len(gens) == 1 and is_sequence(gens[0]): gens = gens[0] if gens == (None,): gens = () elif has_dups(gens): raise GeneratorsError("duplicated generators: %s" % str(gens)) elif any(gen.is_commutative is False for gen in gens): raise GeneratorsError("non-commutative generators: %s" % str(gens)) return tuple(gens) class Wrt(Option, metaclass=OptionType): """``wrt`` option to polynomial manipulation functions. """ option = 'wrt' requires: list[str] = [] excludes: list[str] = [] _re_split = re.compile(r"\s*,\s*|\s+") @classmethod def preprocess(cls, wrt): if isinstance(wrt, Basic): return [str(wrt)] elif isinstance(wrt, str): wrt = wrt.strip() if wrt.endswith(','): raise OptionError('Bad input: missing parameter.') if not wrt: return [] return list(cls._re_split.split(wrt)) elif hasattr(wrt, '__getitem__'): return list(map(str, wrt)) else: raise OptionError("invalid argument for 'wrt' option") class Sort(Option, metaclass=OptionType): """``sort`` option to polynomial manipulation functions. """ option = 'sort' requires: list[str] = [] excludes: list[str] = [] @classmethod def default(cls): return [] @classmethod def preprocess(cls, sort): if isinstance(sort, str): return [ gen.strip() for gen in sort.split('>') ] elif hasattr(sort, '__getitem__'): return list(map(str, sort)) else: raise OptionError("invalid argument for 'sort' option") class Order(Option, metaclass=OptionType): """``order`` option to polynomial manipulation functions. """ option = 'order' requires: list[str] = [] excludes: list[str] = [] @classmethod def default(cls): return sympy.polys.orderings.lex @classmethod def preprocess(cls, order): return sympy.polys.orderings.monomial_key(order) class Field(BooleanOption, metaclass=OptionType): """``field`` option to polynomial manipulation functions. """ option = 'field' requires: list[str] = [] excludes = ['domain', 'split', 'gaussian'] class Greedy(BooleanOption, metaclass=OptionType): """``greedy`` option to polynomial manipulation functions. """ option = 'greedy' requires: list[str] = [] excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric'] class Composite(BooleanOption, metaclass=OptionType): """``composite`` option to polynomial manipulation functions. """ option = 'composite' @classmethod def default(cls): return None requires: list[str] = [] excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric'] class Domain(Option, metaclass=OptionType): """``domain`` option to polynomial manipulation functions. """ option = 'domain' requires: list[str] = [] excludes = ['field', 'greedy', 'split', 'gaussian', 'extension'] after = ['gens'] _re_realfield = re.compile(r"^(R|RR)(_(\d+))?$") _re_complexfield = re.compile(r"^(C|CC)(_(\d+))?$") _re_finitefield = re.compile(r"^(FF|GF)\((\d+)\)$") _re_polynomial = re.compile(r"^(Z|ZZ|Q|QQ|ZZ_I|QQ_I|R|RR|C|CC)\[(.+)\]$") _re_fraction = re.compile(r"^(Z|ZZ|Q|QQ)\((.+)\)$") _re_algebraic = re.compile(r"^(Q|QQ)\<(.+)\>$") @classmethod def preprocess(cls, domain): if isinstance(domain, sympy.polys.domains.Domain): return domain elif hasattr(domain, 'to_domain'): return domain.to_domain() elif isinstance(domain, str): if domain in ['Z', 'ZZ']: return sympy.polys.domains.ZZ if domain in ['Q', 'QQ']: return sympy.polys.domains.QQ if domain == 'ZZ_I': return sympy.polys.domains.ZZ_I if domain == 'QQ_I': return sympy.polys.domains.QQ_I if domain == 'EX': return sympy.polys.domains.EX r = cls._re_realfield.match(domain) if r is not None: _, _, prec = r.groups() if prec is None: return sympy.polys.domains.RR else: return sympy.polys.domains.RealField(int(prec)) r = cls._re_complexfield.match(domain) if r is not None: _, _, prec = r.groups() if prec is None: return sympy.polys.domains.CC else: return sympy.polys.domains.ComplexField(int(prec)) r = cls._re_finitefield.match(domain) if r is not None: return sympy.polys.domains.FF(int(r.groups()[1])) r = cls._re_polynomial.match(domain) if r is not None: ground, gens = r.groups() gens = list(map(sympify, gens.split(','))) if ground in ['Z', 'ZZ']: return sympy.polys.domains.ZZ.poly_ring(*gens) elif ground in ['Q', 'QQ']: return sympy.polys.domains.QQ.poly_ring(*gens) elif ground in ['R', 'RR']: return sympy.polys.domains.RR.poly_ring(*gens) elif ground == 'ZZ_I': return sympy.polys.domains.ZZ_I.poly_ring(*gens) elif ground == 'QQ_I': return sympy.polys.domains.QQ_I.poly_ring(*gens) else: return sympy.polys.domains.CC.poly_ring(*gens) r = cls._re_fraction.match(domain) if r is not None: ground, gens = r.groups() gens = list(map(sympify, gens.split(','))) if ground in ['Z', 'ZZ']: return sympy.polys.domains.ZZ.frac_field(*gens) else: return sympy.polys.domains.QQ.frac_field(*gens) r = cls._re_algebraic.match(domain) if r is not None: gens = list(map(sympify, r.groups()[1].split(','))) return sympy.polys.domains.QQ.algebraic_field(*gens) raise OptionError('expected a valid domain specification, got %s' % domain) @classmethod def postprocess(cls, options): if 'gens' in options and 'domain' in options and options['domain'].is_Composite and \ (set(options['domain'].symbols) & set(options['gens'])): raise GeneratorsError( "ground domain and generators interfere together") elif ('gens' not in options or not options['gens']) and \ 'domain' in options and options['domain'] == sympy.polys.domains.EX: raise GeneratorsError("you have to provide generators because EX domain was requested") class Split(BooleanOption, metaclass=OptionType): """``split`` option to polynomial manipulation functions. """ option = 'split' requires: list[str] = [] excludes = ['field', 'greedy', 'domain', 'gaussian', 'extension', 'modulus', 'symmetric'] @classmethod def postprocess(cls, options): if 'split' in options: raise NotImplementedError("'split' option is not implemented yet") class Gaussian(BooleanOption, metaclass=OptionType): """``gaussian`` option to polynomial manipulation functions. """ option = 'gaussian' requires: list[str] = [] excludes = ['field', 'greedy', 'domain', 'split', 'extension', 'modulus', 'symmetric'] @classmethod def postprocess(cls, options): if 'gaussian' in options and options['gaussian'] is True: options['domain'] = sympy.polys.domains.QQ_I Extension.postprocess(options) class Extension(Option, metaclass=OptionType): """``extension`` option to polynomial manipulation functions. """ option = 'extension' requires: list[str] = [] excludes = ['greedy', 'domain', 'split', 'gaussian', 'modulus', 'symmetric'] @classmethod def preprocess(cls, extension): if extension == 1: return bool(extension) elif extension == 0: raise OptionError("'False' is an invalid argument for 'extension'") else: if not hasattr(extension, '__iter__'): extension = {extension} else: if not extension: extension = None else: extension = set(extension) return extension @classmethod def postprocess(cls, options): if 'extension' in options and options['extension'] is not True: options['domain'] = sympy.polys.domains.QQ.algebraic_field( *options['extension']) class Modulus(Option, metaclass=OptionType): """``modulus`` option to polynomial manipulation functions. """ option = 'modulus' requires: list[str] = [] excludes = ['greedy', 'split', 'domain', 'gaussian', 'extension'] @classmethod def preprocess(cls, modulus): modulus = sympify(modulus) if modulus.is_Integer and modulus > 0: return int(modulus) else: raise OptionError( "'modulus' must a positive integer, got %s" % modulus) @classmethod def postprocess(cls, options): if 'modulus' in options: modulus = options['modulus'] symmetric = options.get('symmetric', True) options['domain'] = sympy.polys.domains.FF(modulus, symmetric) class Symmetric(BooleanOption, metaclass=OptionType): """``symmetric`` option to polynomial manipulation functions. """ option = 'symmetric' requires = ['modulus'] excludes = ['greedy', 'domain', 'split', 'gaussian', 'extension'] class Strict(BooleanOption, metaclass=OptionType): """``strict`` option to polynomial manipulation functions. """ option = 'strict' @classmethod def default(cls): return True class Auto(BooleanOption, Flag, metaclass=OptionType): """``auto`` flag to polynomial manipulation functions. """ option = 'auto' after = ['field', 'domain', 'extension', 'gaussian'] @classmethod def default(cls): return True @classmethod def postprocess(cls, options): if ('domain' in options or 'field' in options) and 'auto' not in options: options['auto'] = False class Frac(BooleanOption, Flag, metaclass=OptionType): """``auto`` option to polynomial manipulation functions. """ option = 'frac' @classmethod def default(cls): return False class Formal(BooleanOption, Flag, metaclass=OptionType): """``formal`` flag to polynomial manipulation functions. """ option = 'formal' @classmethod def default(cls): return False class Polys(BooleanOption, Flag, metaclass=OptionType): """``polys`` flag to polynomial manipulation functions. """ option = 'polys' class Include(BooleanOption, Flag, metaclass=OptionType): """``include`` flag to polynomial manipulation functions. """ option = 'include' @classmethod def default(cls): return False class All(BooleanOption, Flag, metaclass=OptionType): """``all`` flag to polynomial manipulation functions. """ option = 'all' @classmethod def default(cls): return False class Gen(Flag, metaclass=OptionType): """``gen`` flag to polynomial manipulation functions. """ option = 'gen' @classmethod def default(cls): return 0 @classmethod def preprocess(cls, gen): if isinstance(gen, (Basic, int)): return gen else: raise OptionError("invalid argument for 'gen' option") class Series(BooleanOption, Flag, metaclass=OptionType): """``series`` flag to polynomial manipulation functions. """ option = 'series' @classmethod def default(cls): return False class Symbols(Flag, metaclass=OptionType): """``symbols`` flag to polynomial manipulation functions. """ option = 'symbols' @classmethod def default(cls): return numbered_symbols('s', start=1) @classmethod def preprocess(cls, symbols): if hasattr(symbols, '__iter__'): return iter(symbols) else: raise OptionError("expected an iterator or iterable container, got %s" % symbols) class Method(Flag, metaclass=OptionType): """``method`` flag to polynomial manipulation functions. """ option = 'method' @classmethod def preprocess(cls, method): if isinstance(method, str): return method.lower() else: raise OptionError("expected a string, got %s" % method) def build_options(gens, args=None): """Construct options from keyword arguments or ... options. """ if args is None: gens, args = (), gens if len(args) != 1 or 'opt' not in args or gens: return Options(gens, args) else: return args['opt'] def allowed_flags(args, flags): """ Allow specified flags to be used in the given context. Examples ======== >>> from sympy.polys.polyoptions import allowed_flags >>> from sympy.polys.domains import ZZ >>> allowed_flags({'domain': ZZ}, []) >>> allowed_flags({'domain': ZZ, 'frac': True}, []) Traceback (most recent call last): ... FlagError: 'frac' flag is not allowed in this context >>> allowed_flags({'domain': ZZ, 'frac': True}, ['frac']) """ flags = set(flags) for arg in args.keys(): try: if Options.__options__[arg].is_Flag and arg not in flags: raise FlagError( "'%s' flag is not allowed in this context" % arg) except KeyError: raise OptionError("'%s' is not a valid option" % arg) def set_defaults(options, **defaults): """Update options with default values. """ if 'defaults' not in options: options = dict(options) options['defaults'] = defaults return options Options._init_dependencies_order()