Coverage for /usr/lib/python3/dist-packages/sympy/polys/fields.py: 21%

429 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1"""Sparse rational function fields. """ 

2 

3from __future__ import annotations 

4from typing import Any 

5from functools import reduce 

6 

7from operator import add, mul, lt, le, gt, ge 

8 

9from sympy.core.expr import Expr 

10from sympy.core.mod import Mod 

11from sympy.core.numbers import Exp1 

12from sympy.core.singleton import S 

13from sympy.core.symbol import Symbol 

14from sympy.core.sympify import CantSympify, sympify 

15from sympy.functions.elementary.exponential import ExpBase 

16from sympy.polys.domains.domainelement import DomainElement 

17from sympy.polys.domains.fractionfield import FractionField 

18from sympy.polys.domains.polynomialring import PolynomialRing 

19from sympy.polys.constructor import construct_domain 

20from sympy.polys.orderings import lex 

21from sympy.polys.polyerrors import CoercionFailed 

22from sympy.polys.polyoptions import build_options 

23from sympy.polys.polyutils import _parallel_dict_from_expr 

24from sympy.polys.rings import PolyElement 

25from sympy.printing.defaults import DefaultPrinting 

26from sympy.utilities import public 

27from sympy.utilities.iterables import is_sequence 

28from sympy.utilities.magic import pollute 

29 

30@public 

31def field(symbols, domain, order=lex): 

32 """Construct new rational function field returning (field, x1, ..., xn). """ 

33 _field = FracField(symbols, domain, order) 

34 return (_field,) + _field.gens 

35 

36@public 

37def xfield(symbols, domain, order=lex): 

38 """Construct new rational function field returning (field, (x1, ..., xn)). """ 

39 _field = FracField(symbols, domain, order) 

40 return (_field, _field.gens) 

41 

42@public 

43def vfield(symbols, domain, order=lex): 

44 """Construct new rational function field and inject generators into global namespace. """ 

45 _field = FracField(symbols, domain, order) 

46 pollute([ sym.name for sym in _field.symbols ], _field.gens) 

47 return _field 

48 

49@public 

50def sfield(exprs, *symbols, **options): 

51 """Construct a field deriving generators and domain 

52 from options and input expressions. 

53 

54 Parameters 

55 ========== 

56 

57 exprs : py:class:`~.Expr` or sequence of :py:class:`~.Expr` (sympifiable) 

58 

59 symbols : sequence of :py:class:`~.Symbol`/:py:class:`~.Expr` 

60 

61 options : keyword arguments understood by :py:class:`~.Options` 

62 

63 Examples 

64 ======== 

65 

66 >>> from sympy import exp, log, symbols, sfield 

67 

68 >>> x = symbols("x") 

69 >>> K, f = sfield((x*log(x) + 4*x**2)*exp(1/x + log(x)/3)/x**2) 

70 >>> K 

71 Rational function field in x, exp(1/x), log(x), x**(1/3) over ZZ with lex order 

72 >>> f 

73 (4*x**2*(exp(1/x)) + x*(exp(1/x))*(log(x)))/((x**(1/3))**5) 

74 """ 

75 single = False 

76 if not is_sequence(exprs): 

77 exprs, single = [exprs], True 

78 

79 exprs = list(map(sympify, exprs)) 

80 opt = build_options(symbols, options) 

81 numdens = [] 

82 for expr in exprs: 

83 numdens.extend(expr.as_numer_denom()) 

84 reps, opt = _parallel_dict_from_expr(numdens, opt) 

85 

86 if opt.domain is None: 

87 # NOTE: this is inefficient because construct_domain() automatically 

88 # performs conversion to the target domain. It shouldn't do this. 

89 coeffs = sum([list(rep.values()) for rep in reps], []) 

90 opt.domain, _ = construct_domain(coeffs, opt=opt) 

91 

92 _field = FracField(opt.gens, opt.domain, opt.order) 

93 fracs = [] 

94 for i in range(0, len(reps), 2): 

95 fracs.append(_field(tuple(reps[i:i+2]))) 

96 

97 if single: 

98 return (_field, fracs[0]) 

99 else: 

100 return (_field, fracs) 

101 

102_field_cache: dict[Any, Any] = {} 

103 

104class FracField(DefaultPrinting): 

105 """Multivariate distributed rational function field. """ 

106 

107 def __new__(cls, symbols, domain, order=lex): 

108 from sympy.polys.rings import PolyRing 

109 ring = PolyRing(symbols, domain, order) 

110 symbols = ring.symbols 

111 ngens = ring.ngens 

112 domain = ring.domain 

113 order = ring.order 

114 

115 _hash_tuple = (cls.__name__, symbols, ngens, domain, order) 

116 obj = _field_cache.get(_hash_tuple) 

117 

118 if obj is None: 

119 obj = object.__new__(cls) 

120 obj._hash_tuple = _hash_tuple 

121 obj._hash = hash(_hash_tuple) 

122 obj.ring = ring 

123 obj.dtype = type("FracElement", (FracElement,), {"field": obj}) 

124 obj.symbols = symbols 

125 obj.ngens = ngens 

126 obj.domain = domain 

127 obj.order = order 

128 

129 obj.zero = obj.dtype(ring.zero) 

130 obj.one = obj.dtype(ring.one) 

131 

132 obj.gens = obj._gens() 

133 

134 for symbol, generator in zip(obj.symbols, obj.gens): 

135 if isinstance(symbol, Symbol): 

136 name = symbol.name 

137 

138 if not hasattr(obj, name): 

139 setattr(obj, name, generator) 

140 

141 _field_cache[_hash_tuple] = obj 

142 

143 return obj 

144 

145 def _gens(self): 

146 """Return a list of polynomial generators. """ 

147 return tuple([ self.dtype(gen) for gen in self.ring.gens ]) 

148 

149 def __getnewargs__(self): 

150 return (self.symbols, self.domain, self.order) 

151 

152 def __hash__(self): 

153 return self._hash 

154 

155 def index(self, gen): 

156 if isinstance(gen, self.dtype): 

157 return self.ring.index(gen.to_poly()) 

158 else: 

159 raise ValueError("expected a %s, got %s instead" % (self.dtype,gen)) 

160 

161 def __eq__(self, other): 

162 return isinstance(other, FracField) and \ 

163 (self.symbols, self.ngens, self.domain, self.order) == \ 

164 (other.symbols, other.ngens, other.domain, other.order) 

165 

166 def __ne__(self, other): 

167 return not self == other 

168 

169 def raw_new(self, numer, denom=None): 

170 return self.dtype(numer, denom) 

171 def new(self, numer, denom=None): 

172 if denom is None: denom = self.ring.one 

173 numer, denom = numer.cancel(denom) 

174 return self.raw_new(numer, denom) 

175 

176 def domain_new(self, element): 

177 return self.domain.convert(element) 

178 

179 def ground_new(self, element): 

180 try: 

181 return self.new(self.ring.ground_new(element)) 

182 except CoercionFailed: 

183 domain = self.domain 

184 

185 if not domain.is_Field and domain.has_assoc_Field: 

186 ring = self.ring 

187 ground_field = domain.get_field() 

188 element = ground_field.convert(element) 

189 numer = ring.ground_new(ground_field.numer(element)) 

190 denom = ring.ground_new(ground_field.denom(element)) 

191 return self.raw_new(numer, denom) 

192 else: 

193 raise 

194 

195 def field_new(self, element): 

196 if isinstance(element, FracElement): 

197 if self == element.field: 

198 return element 

199 

200 if isinstance(self.domain, FractionField) and \ 

201 self.domain.field == element.field: 

202 return self.ground_new(element) 

203 elif isinstance(self.domain, PolynomialRing) and \ 

204 self.domain.ring.to_field() == element.field: 

205 return self.ground_new(element) 

206 else: 

207 raise NotImplementedError("conversion") 

208 elif isinstance(element, PolyElement): 

209 denom, numer = element.clear_denoms() 

210 

211 if isinstance(self.domain, PolynomialRing) and \ 

212 numer.ring == self.domain.ring: 

213 numer = self.ring.ground_new(numer) 

214 elif isinstance(self.domain, FractionField) and \ 

215 numer.ring == self.domain.field.to_ring(): 

216 numer = self.ring.ground_new(numer) 

217 else: 

218 numer = numer.set_ring(self.ring) 

219 

220 denom = self.ring.ground_new(denom) 

221 return self.raw_new(numer, denom) 

222 elif isinstance(element, tuple) and len(element) == 2: 

223 numer, denom = list(map(self.ring.ring_new, element)) 

224 return self.new(numer, denom) 

225 elif isinstance(element, str): 

226 raise NotImplementedError("parsing") 

227 elif isinstance(element, Expr): 

228 return self.from_expr(element) 

229 else: 

230 return self.ground_new(element) 

231 

232 __call__ = field_new 

233 

234 def _rebuild_expr(self, expr, mapping): 

235 domain = self.domain 

236 powers = tuple((gen, gen.as_base_exp()) for gen in mapping.keys() 

237 if gen.is_Pow or isinstance(gen, ExpBase)) 

238 

239 def _rebuild(expr): 

240 generator = mapping.get(expr) 

241 

242 if generator is not None: 

243 return generator 

244 elif expr.is_Add: 

245 return reduce(add, list(map(_rebuild, expr.args))) 

246 elif expr.is_Mul: 

247 return reduce(mul, list(map(_rebuild, expr.args))) 

248 elif expr.is_Pow or isinstance(expr, (ExpBase, Exp1)): 

249 b, e = expr.as_base_exp() 

250 # look for bg**eg whose integer power may be b**e 

251 for gen, (bg, eg) in powers: 

252 if bg == b and Mod(e, eg) == 0: 

253 return mapping.get(gen)**int(e/eg) 

254 if e.is_Integer and e is not S.One: 

255 return _rebuild(b)**int(e) 

256 elif mapping.get(1/expr) is not None: 

257 return 1/mapping.get(1/expr) 

258 

259 try: 

260 return domain.convert(expr) 

261 except CoercionFailed: 

262 if not domain.is_Field and domain.has_assoc_Field: 

263 return domain.get_field().convert(expr) 

264 else: 

265 raise 

266 

267 return _rebuild(expr) 

268 

269 def from_expr(self, expr): 

270 mapping = dict(list(zip(self.symbols, self.gens))) 

271 

272 try: 

273 frac = self._rebuild_expr(sympify(expr), mapping) 

274 except CoercionFailed: 

275 raise ValueError("expected an expression convertible to a rational function in %s, got %s" % (self, expr)) 

276 else: 

277 return self.field_new(frac) 

278 

279 def to_domain(self): 

280 return FractionField(self) 

281 

282 def to_ring(self): 

283 from sympy.polys.rings import PolyRing 

284 return PolyRing(self.symbols, self.domain, self.order) 

285 

286class FracElement(DomainElement, DefaultPrinting, CantSympify): 

287 """Element of multivariate distributed rational function field. """ 

288 

289 def __init__(self, numer, denom=None): 

290 if denom is None: 

291 denom = self.field.ring.one 

292 elif not denom: 

293 raise ZeroDivisionError("zero denominator") 

294 

295 self.numer = numer 

296 self.denom = denom 

297 

298 def raw_new(f, numer, denom): 

299 return f.__class__(numer, denom) 

300 def new(f, numer, denom): 

301 return f.raw_new(*numer.cancel(denom)) 

302 

303 def to_poly(f): 

304 if f.denom != 1: 

305 raise ValueError("f.denom should be 1") 

306 return f.numer 

307 

308 def parent(self): 

309 return self.field.to_domain() 

310 

311 def __getnewargs__(self): 

312 return (self.field, self.numer, self.denom) 

313 

314 _hash = None 

315 

316 def __hash__(self): 

317 _hash = self._hash 

318 if _hash is None: 

319 self._hash = _hash = hash((self.field, self.numer, self.denom)) 

320 return _hash 

321 

322 def copy(self): 

323 return self.raw_new(self.numer.copy(), self.denom.copy()) 

324 

325 def set_field(self, new_field): 

326 if self.field == new_field: 

327 return self 

328 else: 

329 new_ring = new_field.ring 

330 numer = self.numer.set_ring(new_ring) 

331 denom = self.denom.set_ring(new_ring) 

332 return new_field.new(numer, denom) 

333 

334 def as_expr(self, *symbols): 

335 return self.numer.as_expr(*symbols)/self.denom.as_expr(*symbols) 

336 

337 def __eq__(f, g): 

338 if isinstance(g, FracElement) and f.field == g.field: 

339 return f.numer == g.numer and f.denom == g.denom 

340 else: 

341 return f.numer == g and f.denom == f.field.ring.one 

342 

343 def __ne__(f, g): 

344 return not f == g 

345 

346 def __bool__(f): 

347 return bool(f.numer) 

348 

349 def sort_key(self): 

350 return (self.denom.sort_key(), self.numer.sort_key()) 

351 

352 def _cmp(f1, f2, op): 

353 if isinstance(f2, f1.field.dtype): 

354 return op(f1.sort_key(), f2.sort_key()) 

355 else: 

356 return NotImplemented 

357 

358 def __lt__(f1, f2): 

359 return f1._cmp(f2, lt) 

360 def __le__(f1, f2): 

361 return f1._cmp(f2, le) 

362 def __gt__(f1, f2): 

363 return f1._cmp(f2, gt) 

364 def __ge__(f1, f2): 

365 return f1._cmp(f2, ge) 

366 

367 def __pos__(f): 

368 """Negate all coefficients in ``f``. """ 

369 return f.raw_new(f.numer, f.denom) 

370 

371 def __neg__(f): 

372 """Negate all coefficients in ``f``. """ 

373 return f.raw_new(-f.numer, f.denom) 

374 

375 def _extract_ground(self, element): 

376 domain = self.field.domain 

377 

378 try: 

379 element = domain.convert(element) 

380 except CoercionFailed: 

381 if not domain.is_Field and domain.has_assoc_Field: 

382 ground_field = domain.get_field() 

383 

384 try: 

385 element = ground_field.convert(element) 

386 except CoercionFailed: 

387 pass 

388 else: 

389 return -1, ground_field.numer(element), ground_field.denom(element) 

390 

391 return 0, None, None 

392 else: 

393 return 1, element, None 

394 

395 def __add__(f, g): 

396 """Add rational functions ``f`` and ``g``. """ 

397 field = f.field 

398 

399 if not g: 

400 return f 

401 elif not f: 

402 return g 

403 elif isinstance(g, field.dtype): 

404 if f.denom == g.denom: 

405 return f.new(f.numer + g.numer, f.denom) 

406 else: 

407 return f.new(f.numer*g.denom + f.denom*g.numer, f.denom*g.denom) 

408 elif isinstance(g, field.ring.dtype): 

409 return f.new(f.numer + f.denom*g, f.denom) 

410 else: 

411 if isinstance(g, FracElement): 

412 if isinstance(field.domain, FractionField) and field.domain.field == g.field: 

413 pass 

414 elif isinstance(g.field.domain, FractionField) and g.field.domain.field == field: 

415 return g.__radd__(f) 

416 else: 

417 return NotImplemented 

418 elif isinstance(g, PolyElement): 

419 if isinstance(field.domain, PolynomialRing) and field.domain.ring == g.ring: 

420 pass 

421 else: 

422 return g.__radd__(f) 

423 

424 return f.__radd__(g) 

425 

426 def __radd__(f, c): 

427 if isinstance(c, f.field.ring.dtype): 

428 return f.new(f.numer + f.denom*c, f.denom) 

429 

430 op, g_numer, g_denom = f._extract_ground(c) 

431 

432 if op == 1: 

433 return f.new(f.numer + f.denom*g_numer, f.denom) 

434 elif not op: 

435 return NotImplemented 

436 else: 

437 return f.new(f.numer*g_denom + f.denom*g_numer, f.denom*g_denom) 

438 

439 def __sub__(f, g): 

440 """Subtract rational functions ``f`` and ``g``. """ 

441 field = f.field 

442 

443 if not g: 

444 return f 

445 elif not f: 

446 return -g 

447 elif isinstance(g, field.dtype): 

448 if f.denom == g.denom: 

449 return f.new(f.numer - g.numer, f.denom) 

450 else: 

451 return f.new(f.numer*g.denom - f.denom*g.numer, f.denom*g.denom) 

452 elif isinstance(g, field.ring.dtype): 

453 return f.new(f.numer - f.denom*g, f.denom) 

454 else: 

455 if isinstance(g, FracElement): 

456 if isinstance(field.domain, FractionField) and field.domain.field == g.field: 

457 pass 

458 elif isinstance(g.field.domain, FractionField) and g.field.domain.field == field: 

459 return g.__rsub__(f) 

460 else: 

461 return NotImplemented 

462 elif isinstance(g, PolyElement): 

463 if isinstance(field.domain, PolynomialRing) and field.domain.ring == g.ring: 

464 pass 

465 else: 

466 return g.__rsub__(f) 

467 

468 op, g_numer, g_denom = f._extract_ground(g) 

469 

470 if op == 1: 

471 return f.new(f.numer - f.denom*g_numer, f.denom) 

472 elif not op: 

473 return NotImplemented 

474 else: 

475 return f.new(f.numer*g_denom - f.denom*g_numer, f.denom*g_denom) 

476 

477 def __rsub__(f, c): 

478 if isinstance(c, f.field.ring.dtype): 

479 return f.new(-f.numer + f.denom*c, f.denom) 

480 

481 op, g_numer, g_denom = f._extract_ground(c) 

482 

483 if op == 1: 

484 return f.new(-f.numer + f.denom*g_numer, f.denom) 

485 elif not op: 

486 return NotImplemented 

487 else: 

488 return f.new(-f.numer*g_denom + f.denom*g_numer, f.denom*g_denom) 

489 

490 def __mul__(f, g): 

491 """Multiply rational functions ``f`` and ``g``. """ 

492 field = f.field 

493 

494 if not f or not g: 

495 return field.zero 

496 elif isinstance(g, field.dtype): 

497 return f.new(f.numer*g.numer, f.denom*g.denom) 

498 elif isinstance(g, field.ring.dtype): 

499 return f.new(f.numer*g, f.denom) 

500 else: 

501 if isinstance(g, FracElement): 

502 if isinstance(field.domain, FractionField) and field.domain.field == g.field: 

503 pass 

504 elif isinstance(g.field.domain, FractionField) and g.field.domain.field == field: 

505 return g.__rmul__(f) 

506 else: 

507 return NotImplemented 

508 elif isinstance(g, PolyElement): 

509 if isinstance(field.domain, PolynomialRing) and field.domain.ring == g.ring: 

510 pass 

511 else: 

512 return g.__rmul__(f) 

513 

514 return f.__rmul__(g) 

515 

516 def __rmul__(f, c): 

517 if isinstance(c, f.field.ring.dtype): 

518 return f.new(f.numer*c, f.denom) 

519 

520 op, g_numer, g_denom = f._extract_ground(c) 

521 

522 if op == 1: 

523 return f.new(f.numer*g_numer, f.denom) 

524 elif not op: 

525 return NotImplemented 

526 else: 

527 return f.new(f.numer*g_numer, f.denom*g_denom) 

528 

529 def __truediv__(f, g): 

530 """Computes quotient of fractions ``f`` and ``g``. """ 

531 field = f.field 

532 

533 if not g: 

534 raise ZeroDivisionError 

535 elif isinstance(g, field.dtype): 

536 return f.new(f.numer*g.denom, f.denom*g.numer) 

537 elif isinstance(g, field.ring.dtype): 

538 return f.new(f.numer, f.denom*g) 

539 else: 

540 if isinstance(g, FracElement): 

541 if isinstance(field.domain, FractionField) and field.domain.field == g.field: 

542 pass 

543 elif isinstance(g.field.domain, FractionField) and g.field.domain.field == field: 

544 return g.__rtruediv__(f) 

545 else: 

546 return NotImplemented 

547 elif isinstance(g, PolyElement): 

548 if isinstance(field.domain, PolynomialRing) and field.domain.ring == g.ring: 

549 pass 

550 else: 

551 return g.__rtruediv__(f) 

552 

553 op, g_numer, g_denom = f._extract_ground(g) 

554 

555 if op == 1: 

556 return f.new(f.numer, f.denom*g_numer) 

557 elif not op: 

558 return NotImplemented 

559 else: 

560 return f.new(f.numer*g_denom, f.denom*g_numer) 

561 

562 def __rtruediv__(f, c): 

563 if not f: 

564 raise ZeroDivisionError 

565 elif isinstance(c, f.field.ring.dtype): 

566 return f.new(f.denom*c, f.numer) 

567 

568 op, g_numer, g_denom = f._extract_ground(c) 

569 

570 if op == 1: 

571 return f.new(f.denom*g_numer, f.numer) 

572 elif not op: 

573 return NotImplemented 

574 else: 

575 return f.new(f.denom*g_numer, f.numer*g_denom) 

576 

577 def __pow__(f, n): 

578 """Raise ``f`` to a non-negative power ``n``. """ 

579 if n >= 0: 

580 return f.raw_new(f.numer**n, f.denom**n) 

581 elif not f: 

582 raise ZeroDivisionError 

583 else: 

584 return f.raw_new(f.denom**-n, f.numer**-n) 

585 

586 def diff(f, x): 

587 """Computes partial derivative in ``x``. 

588 

589 Examples 

590 ======== 

591 

592 >>> from sympy.polys.fields import field 

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

594 

595 >>> _, x, y, z = field("x,y,z", ZZ) 

596 >>> ((x**2 + y)/(z + 1)).diff(x) 

597 2*x/(z + 1) 

598 

599 """ 

600 x = x.to_poly() 

601 return f.new(f.numer.diff(x)*f.denom - f.numer*f.denom.diff(x), f.denom**2) 

602 

603 def __call__(f, *values): 

604 if 0 < len(values) <= f.field.ngens: 

605 return f.evaluate(list(zip(f.field.gens, values))) 

606 else: 

607 raise ValueError("expected at least 1 and at most %s values, got %s" % (f.field.ngens, len(values))) 

608 

609 def evaluate(f, x, a=None): 

610 if isinstance(x, list) and a is None: 

611 x = [ (X.to_poly(), a) for X, a in x ] 

612 numer, denom = f.numer.evaluate(x), f.denom.evaluate(x) 

613 else: 

614 x = x.to_poly() 

615 numer, denom = f.numer.evaluate(x, a), f.denom.evaluate(x, a) 

616 

617 field = numer.ring.to_field() 

618 return field.new(numer, denom) 

619 

620 def subs(f, x, a=None): 

621 if isinstance(x, list) and a is None: 

622 x = [ (X.to_poly(), a) for X, a in x ] 

623 numer, denom = f.numer.subs(x), f.denom.subs(x) 

624 else: 

625 x = x.to_poly() 

626 numer, denom = f.numer.subs(x, a), f.denom.subs(x, a) 

627 

628 return f.new(numer, denom) 

629 

630 def compose(f, x, a=None): 

631 raise NotImplementedError