polyoptions.py 21 KB

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