polyoptions.py 21 KB

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