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

1"""Options manager for :class:`~.Poly` and public API functions. """ 

2 

3from __future__ import annotations 

4 

5__all__ = ["Options"] 

6 

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 

11 

12import sympy.polys 

13 

14import re 

15 

16class Option: 

17 """Base class for all kinds of options. """ 

18 

19 option: str | None = None 

20 

21 is_Flag = False 

22 

23 requires: list[str] = [] 

24 excludes: list[str] = [] 

25 

26 after: list[str] = [] 

27 before: list[str] = [] 

28 

29 @classmethod 

30 def default(cls): 

31 return None 

32 

33 @classmethod 

34 def preprocess(cls, option): 

35 return None 

36 

37 @classmethod 

38 def postprocess(cls, options): 

39 pass 

40 

41 

42class Flag(Option): 

43 """Base class for all kinds of flags. """ 

44 

45 is_Flag = True 

46 

47 

48class BooleanOption(Option): 

49 """An option that must have a boolean value or equivalent assigned. """ 

50 

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)) 

57 

58 

59class OptionType(type): 

60 """Base type for all options that does registers options. """ 

61 

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() 

69 

70 setattr(Options, cls.option, getter) 

71 Options.__options__[cls.option] = cls 

72 

73 

74@public 

75class Options(dict): 

76 """ 

77 Options manager for polynomial manipulation module. 

78 

79 Examples 

80 ======== 

81 

82 >>> from sympy.polys.polyoptions import Options 

83 >>> from sympy.polys.polyoptions import build_options 

84 

85 >>> from sympy.abc import x, y, z 

86 

87 >>> Options((x, y, z), {'domain': 'ZZ'}) 

88 {'auto': False, 'domain': ZZ, 'gens': (x, y, z)} 

89 

90 >>> build_options((x, y, z), {'domain': 'ZZ'}) 

91 {'auto': False, 'domain': ZZ, 'gens': (x, y, z)} 

92 

93 **Options** 

94 

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 

109 

110 **Flags** 

111 

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 

120 

121 """ 

122 

123 __order__ = None 

124 __options__: dict[str, type[Option]] = {} 

125 

126 def __init__(self, gens, args, flags=None, strict=False): 

127 dict.__init__(self) 

128 

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 

135 

136 defaults = args.pop('defaults', {}) 

137 

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) 

144 

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) 

149 

150 if value is not None: 

151 self[option] = cls.preprocess(value) 

152 

153 preprocess_options(args) 

154 

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] 

161 

162 if key in cls.excludes: 

163 del defaults[key] 

164 break 

165 

166 preprocess_options(defaults) 

167 

168 for option in self.keys(): 

169 cls = self.__options__[option] 

170 

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)) 

174 

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)) 

178 

179 for option in self.__order__: 

180 self.__options__[option].postprocess(self) 

181 

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() 

187 

188 for name, option in cls.__options__.items(): 

189 vertices.append(name) 

190 

191 for _name in option.after: 

192 edges.add((_name, name)) 

193 

194 for _name in option.before: 

195 edges.add((name, _name)) 

196 

197 try: 

198 cls.__order__ = topological_sort((vertices, list(edges))) 

199 except ValueError: 

200 raise RuntimeError( 

201 "cycle detected in sympy.polys options framework") 

202 

203 def clone(self, updates={}): 

204 """Clone ``self`` and update specified options. """ 

205 obj = dict.__new__(self.__class__) 

206 

207 for option, value in self.items(): 

208 obj[option] = value 

209 

210 for option, value in updates.items(): 

211 obj[option] = value 

212 

213 return obj 

214 

215 def __setattr__(self, attr, value): 

216 if attr in self.__options__: 

217 self[attr] = value 

218 else: 

219 super().__setattr__(attr, value) 

220 

221 @property 

222 def args(self): 

223 args = {} 

224 

225 for option, value in self.items(): 

226 if value is not None and option != 'gens': 

227 cls = self.__options__[option] 

228 

229 if not issubclass(cls, Flag): 

230 args[option] = value 

231 

232 return args 

233 

234 @property 

235 def options(self): 

236 options = {} 

237 

238 for option, cls in self.__options__.items(): 

239 if not issubclass(cls, Flag): 

240 options[option] = getattr(self, option) 

241 

242 return options 

243 

244 @property 

245 def flags(self): 

246 flags = {} 

247 

248 for option, cls in self.__options__.items(): 

249 if issubclass(cls, Flag): 

250 flags[option] = getattr(self, option) 

251 

252 return flags 

253 

254 

255class Expand(BooleanOption, metaclass=OptionType): 

256 """``expand`` option to polynomial manipulation functions. """ 

257 

258 option = 'expand' 

259 

260 requires: list[str] = [] 

261 excludes: list[str] = [] 

262 

263 @classmethod 

264 def default(cls): 

265 return True 

266 

267 

268class Gens(Option, metaclass=OptionType): 

269 """``gens`` option to polynomial manipulation functions. """ 

270 

271 option = 'gens' 

272 

273 requires: list[str] = [] 

274 excludes: list[str] = [] 

275 

276 @classmethod 

277 def default(cls): 

278 return () 

279 

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] 

286 

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)) 

293 

294 return tuple(gens) 

295 

296 

297class Wrt(Option, metaclass=OptionType): 

298 """``wrt`` option to polynomial manipulation functions. """ 

299 

300 option = 'wrt' 

301 

302 requires: list[str] = [] 

303 excludes: list[str] = [] 

304 

305 _re_split = re.compile(r"\s*,\s*|\s+") 

306 

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") 

322 

323 

324class Sort(Option, metaclass=OptionType): 

325 """``sort`` option to polynomial manipulation functions. """ 

326 

327 option = 'sort' 

328 

329 requires: list[str] = [] 

330 excludes: list[str] = [] 

331 

332 @classmethod 

333 def default(cls): 

334 return [] 

335 

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") 

344 

345 

346class Order(Option, metaclass=OptionType): 

347 """``order`` option to polynomial manipulation functions. """ 

348 

349 option = 'order' 

350 

351 requires: list[str] = [] 

352 excludes: list[str] = [] 

353 

354 @classmethod 

355 def default(cls): 

356 return sympy.polys.orderings.lex 

357 

358 @classmethod 

359 def preprocess(cls, order): 

360 return sympy.polys.orderings.monomial_key(order) 

361 

362 

363class Field(BooleanOption, metaclass=OptionType): 

364 """``field`` option to polynomial manipulation functions. """ 

365 

366 option = 'field' 

367 

368 requires: list[str] = [] 

369 excludes = ['domain', 'split', 'gaussian'] 

370 

371 

372class Greedy(BooleanOption, metaclass=OptionType): 

373 """``greedy`` option to polynomial manipulation functions. """ 

374 

375 option = 'greedy' 

376 

377 requires: list[str] = [] 

378 excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric'] 

379 

380 

381class Composite(BooleanOption, metaclass=OptionType): 

382 """``composite`` option to polynomial manipulation functions. """ 

383 

384 option = 'composite' 

385 

386 @classmethod 

387 def default(cls): 

388 return None 

389 

390 requires: list[str] = [] 

391 excludes = ['domain', 'split', 'gaussian', 'extension', 'modulus', 'symmetric'] 

392 

393 

394class Domain(Option, metaclass=OptionType): 

395 """``domain`` option to polynomial manipulation functions. """ 

396 

397 option = 'domain' 

398 

399 requires: list[str] = [] 

400 excludes = ['field', 'greedy', 'split', 'gaussian', 'extension'] 

401 

402 after = ['gens'] 

403 

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)\<(.+)\>$") 

410 

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 

420 

421 if domain in ['Q', 'QQ']: 

422 return sympy.polys.domains.QQ 

423 

424 if domain == 'ZZ_I': 

425 return sympy.polys.domains.ZZ_I 

426 

427 if domain == 'QQ_I': 

428 return sympy.polys.domains.QQ_I 

429 

430 if domain == 'EX': 

431 return sympy.polys.domains.EX 

432 

433 r = cls._re_realfield.match(domain) 

434 

435 if r is not None: 

436 _, _, prec = r.groups() 

437 

438 if prec is None: 

439 return sympy.polys.domains.RR 

440 else: 

441 return sympy.polys.domains.RealField(int(prec)) 

442 

443 r = cls._re_complexfield.match(domain) 

444 

445 if r is not None: 

446 _, _, prec = r.groups() 

447 

448 if prec is None: 

449 return sympy.polys.domains.CC 

450 else: 

451 return sympy.polys.domains.ComplexField(int(prec)) 

452 

453 r = cls._re_finitefield.match(domain) 

454 

455 if r is not None: 

456 return sympy.polys.domains.FF(int(r.groups()[1])) 

457 

458 r = cls._re_polynomial.match(domain) 

459 

460 if r is not None: 

461 ground, gens = r.groups() 

462 

463 gens = list(map(sympify, gens.split(','))) 

464 

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) 

477 

478 r = cls._re_fraction.match(domain) 

479 

480 if r is not None: 

481 ground, gens = r.groups() 

482 

483 gens = list(map(sympify, gens.split(','))) 

484 

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) 

489 

490 r = cls._re_algebraic.match(domain) 

491 

492 if r is not None: 

493 gens = list(map(sympify, r.groups()[1].split(','))) 

494 return sympy.polys.domains.QQ.algebraic_field(*gens) 

495 

496 raise OptionError('expected a valid domain specification, got %s' % domain) 

497 

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") 

507 

508 

509class Split(BooleanOption, metaclass=OptionType): 

510 """``split`` option to polynomial manipulation functions. """ 

511 

512 option = 'split' 

513 

514 requires: list[str] = [] 

515 excludes = ['field', 'greedy', 'domain', 'gaussian', 'extension', 

516 'modulus', 'symmetric'] 

517 

518 @classmethod 

519 def postprocess(cls, options): 

520 if 'split' in options: 

521 raise NotImplementedError("'split' option is not implemented yet") 

522 

523 

524class Gaussian(BooleanOption, metaclass=OptionType): 

525 """``gaussian`` option to polynomial manipulation functions. """ 

526 

527 option = 'gaussian' 

528 

529 requires: list[str] = [] 

530 excludes = ['field', 'greedy', 'domain', 'split', 'extension', 

531 'modulus', 'symmetric'] 

532 

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) 

538 

539 

540class Extension(Option, metaclass=OptionType): 

541 """``extension`` option to polynomial manipulation functions. """ 

542 

543 option = 'extension' 

544 

545 requires: list[str] = [] 

546 excludes = ['greedy', 'domain', 'split', 'gaussian', 'modulus', 

547 'symmetric'] 

548 

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) 

563 

564 return extension 

565 

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']) 

571 

572 

573class Modulus(Option, metaclass=OptionType): 

574 """``modulus`` option to polynomial manipulation functions. """ 

575 

576 option = 'modulus' 

577 

578 requires: list[str] = [] 

579 excludes = ['greedy', 'split', 'domain', 'gaussian', 'extension'] 

580 

581 @classmethod 

582 def preprocess(cls, modulus): 

583 modulus = sympify(modulus) 

584 

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) 

590 

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) 

597 

598 

599class Symmetric(BooleanOption, metaclass=OptionType): 

600 """``symmetric`` option to polynomial manipulation functions. """ 

601 

602 option = 'symmetric' 

603 

604 requires = ['modulus'] 

605 excludes = ['greedy', 'domain', 'split', 'gaussian', 'extension'] 

606 

607 

608class Strict(BooleanOption, metaclass=OptionType): 

609 """``strict`` option to polynomial manipulation functions. """ 

610 

611 option = 'strict' 

612 

613 @classmethod 

614 def default(cls): 

615 return True 

616 

617 

618class Auto(BooleanOption, Flag, metaclass=OptionType): 

619 """``auto`` flag to polynomial manipulation functions. """ 

620 

621 option = 'auto' 

622 

623 after = ['field', 'domain', 'extension', 'gaussian'] 

624 

625 @classmethod 

626 def default(cls): 

627 return True 

628 

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 

633 

634 

635class Frac(BooleanOption, Flag, metaclass=OptionType): 

636 """``auto`` option to polynomial manipulation functions. """ 

637 

638 option = 'frac' 

639 

640 @classmethod 

641 def default(cls): 

642 return False 

643 

644 

645class Formal(BooleanOption, Flag, metaclass=OptionType): 

646 """``formal`` flag to polynomial manipulation functions. """ 

647 

648 option = 'formal' 

649 

650 @classmethod 

651 def default(cls): 

652 return False 

653 

654 

655class Polys(BooleanOption, Flag, metaclass=OptionType): 

656 """``polys`` flag to polynomial manipulation functions. """ 

657 

658 option = 'polys' 

659 

660 

661class Include(BooleanOption, Flag, metaclass=OptionType): 

662 """``include`` flag to polynomial manipulation functions. """ 

663 

664 option = 'include' 

665 

666 @classmethod 

667 def default(cls): 

668 return False 

669 

670 

671class All(BooleanOption, Flag, metaclass=OptionType): 

672 """``all`` flag to polynomial manipulation functions. """ 

673 

674 option = 'all' 

675 

676 @classmethod 

677 def default(cls): 

678 return False 

679 

680 

681class Gen(Flag, metaclass=OptionType): 

682 """``gen`` flag to polynomial manipulation functions. """ 

683 

684 option = 'gen' 

685 

686 @classmethod 

687 def default(cls): 

688 return 0 

689 

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") 

696 

697 

698class Series(BooleanOption, Flag, metaclass=OptionType): 

699 """``series`` flag to polynomial manipulation functions. """ 

700 

701 option = 'series' 

702 

703 @classmethod 

704 def default(cls): 

705 return False 

706 

707 

708class Symbols(Flag, metaclass=OptionType): 

709 """``symbols`` flag to polynomial manipulation functions. """ 

710 

711 option = 'symbols' 

712 

713 @classmethod 

714 def default(cls): 

715 return numbered_symbols('s', start=1) 

716 

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) 

723 

724 

725class Method(Flag, metaclass=OptionType): 

726 """``method`` flag to polynomial manipulation functions. """ 

727 

728 option = 'method' 

729 

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) 

736 

737 

738def build_options(gens, args=None): 

739 """Construct options from keyword arguments or ... options. """ 

740 if args is None: 

741 gens, args = (), gens 

742 

743 if len(args) != 1 or 'opt' not in args or gens: 

744 return Options(gens, args) 

745 else: 

746 return args['opt'] 

747 

748 

749def allowed_flags(args, flags): 

750 """ 

751 Allow specified flags to be used in the given context. 

752 

753 Examples 

754 ======== 

755 

756 >>> from sympy.polys.polyoptions import allowed_flags 

757 >>> from sympy.polys.domains import ZZ 

758 

759 >>> allowed_flags({'domain': ZZ}, []) 

760 

761 >>> allowed_flags({'domain': ZZ, 'frac': True}, []) 

762 Traceback (most recent call last): 

763 ... 

764 FlagError: 'frac' flag is not allowed in this context 

765 

766 >>> allowed_flags({'domain': ZZ, 'frac': True}, ['frac']) 

767 

768 """ 

769 flags = set(flags) 

770 

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) 

778 

779 

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 

785 

786 return options 

787 

788Options._init_dependencies_order()