polyoptions.py 21 KB

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