Coverage for /usr/lib/python3/dist-packages/sympy/polys/constructor.py: 20%

211 statements  

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

1"""Tools for constructing domains for expressions. """ 

2from math import prod 

3 

4from sympy.core import sympify 

5from sympy.core.evalf import pure_complex 

6from sympy.core.sorting import ordered 

7from sympy.polys.domains import ZZ, QQ, ZZ_I, QQ_I, EX 

8from sympy.polys.domains.complexfield import ComplexField 

9from sympy.polys.domains.realfield import RealField 

10from sympy.polys.polyoptions import build_options 

11from sympy.polys.polyutils import parallel_dict_from_basic 

12from sympy.utilities import public 

13 

14 

15def _construct_simple(coeffs, opt): 

16 """Handle simple domains, e.g.: ZZ, QQ, RR and algebraic domains. """ 

17 rationals = floats = complexes = algebraics = False 

18 float_numbers = [] 

19 

20 if opt.extension is True: 

21 is_algebraic = lambda coeff: coeff.is_number and coeff.is_algebraic 

22 else: 

23 is_algebraic = lambda coeff: False 

24 

25 for coeff in coeffs: 

26 if coeff.is_Rational: 

27 if not coeff.is_Integer: 

28 rationals = True 

29 elif coeff.is_Float: 

30 if algebraics: 

31 # there are both reals and algebraics -> EX 

32 return False 

33 else: 

34 floats = True 

35 float_numbers.append(coeff) 

36 else: 

37 is_complex = pure_complex(coeff) 

38 if is_complex: 

39 complexes = True 

40 x, y = is_complex 

41 if x.is_Rational and y.is_Rational: 

42 if not (x.is_Integer and y.is_Integer): 

43 rationals = True 

44 continue 

45 else: 

46 floats = True 

47 if x.is_Float: 

48 float_numbers.append(x) 

49 if y.is_Float: 

50 float_numbers.append(y) 

51 elif is_algebraic(coeff): 

52 if floats: 

53 # there are both algebraics and reals -> EX 

54 return False 

55 algebraics = True 

56 else: 

57 # this is a composite domain, e.g. ZZ[X], EX 

58 return None 

59 

60 # Use the maximum precision of all coefficients for the RR or CC 

61 # precision 

62 max_prec = max(c._prec for c in float_numbers) if float_numbers else 53 

63 

64 if algebraics: 

65 domain, result = _construct_algebraic(coeffs, opt) 

66 else: 

67 if floats and complexes: 

68 domain = ComplexField(prec=max_prec) 

69 elif floats: 

70 domain = RealField(prec=max_prec) 

71 elif rationals or opt.field: 

72 domain = QQ_I if complexes else QQ 

73 else: 

74 domain = ZZ_I if complexes else ZZ 

75 

76 result = [domain.from_sympy(coeff) for coeff in coeffs] 

77 

78 return domain, result 

79 

80 

81def _construct_algebraic(coeffs, opt): 

82 """We know that coefficients are algebraic so construct the extension. """ 

83 from sympy.polys.numberfields import primitive_element 

84 

85 exts = set() 

86 

87 def build_trees(args): 

88 trees = [] 

89 for a in args: 

90 if a.is_Rational: 

91 tree = ('Q', QQ.from_sympy(a)) 

92 elif a.is_Add: 

93 tree = ('+', build_trees(a.args)) 

94 elif a.is_Mul: 

95 tree = ('*', build_trees(a.args)) 

96 else: 

97 tree = ('e', a) 

98 exts.add(a) 

99 trees.append(tree) 

100 return trees 

101 

102 trees = build_trees(coeffs) 

103 exts = list(ordered(exts)) 

104 

105 g, span, H = primitive_element(exts, ex=True, polys=True) 

106 root = sum([ s*ext for s, ext in zip(span, exts) ]) 

107 

108 domain, g = QQ.algebraic_field((g, root)), g.rep.rep 

109 

110 exts_dom = [domain.dtype.from_list(h, g, QQ) for h in H] 

111 exts_map = dict(zip(exts, exts_dom)) 

112 

113 def convert_tree(tree): 

114 op, args = tree 

115 if op == 'Q': 

116 return domain.dtype.from_list([args], g, QQ) 

117 elif op == '+': 

118 return sum((convert_tree(a) for a in args), domain.zero) 

119 elif op == '*': 

120 return prod(convert_tree(a) for a in args) 

121 elif op == 'e': 

122 return exts_map[args] 

123 else: 

124 raise RuntimeError 

125 

126 result = [convert_tree(tree) for tree in trees] 

127 

128 return domain, result 

129 

130 

131def _construct_composite(coeffs, opt): 

132 """Handle composite domains, e.g.: ZZ[X], QQ[X], ZZ(X), QQ(X). """ 

133 numers, denoms = [], [] 

134 

135 for coeff in coeffs: 

136 numer, denom = coeff.as_numer_denom() 

137 

138 numers.append(numer) 

139 denoms.append(denom) 

140 

141 polys, gens = parallel_dict_from_basic(numers + denoms) # XXX: sorting 

142 if not gens: 

143 return None 

144 

145 if opt.composite is None: 

146 if any(gen.is_number and gen.is_algebraic for gen in gens): 

147 return None # generators are number-like so lets better use EX 

148 

149 all_symbols = set() 

150 

151 for gen in gens: 

152 symbols = gen.free_symbols 

153 

154 if all_symbols & symbols: 

155 return None # there could be algebraic relations between generators 

156 else: 

157 all_symbols |= symbols 

158 

159 n = len(gens) 

160 k = len(polys)//2 

161 

162 numers = polys[:k] 

163 denoms = polys[k:] 

164 

165 if opt.field: 

166 fractions = True 

167 else: 

168 fractions, zeros = False, (0,)*n 

169 

170 for denom in denoms: 

171 if len(denom) > 1 or zeros not in denom: 

172 fractions = True 

173 break 

174 

175 coeffs = set() 

176 

177 if not fractions: 

178 for numer, denom in zip(numers, denoms): 

179 denom = denom[zeros] 

180 

181 for monom, coeff in numer.items(): 

182 coeff /= denom 

183 coeffs.add(coeff) 

184 numer[monom] = coeff 

185 else: 

186 for numer, denom in zip(numers, denoms): 

187 coeffs.update(list(numer.values())) 

188 coeffs.update(list(denom.values())) 

189 

190 rationals = floats = complexes = False 

191 float_numbers = [] 

192 

193 for coeff in coeffs: 

194 if coeff.is_Rational: 

195 if not coeff.is_Integer: 

196 rationals = True 

197 elif coeff.is_Float: 

198 floats = True 

199 float_numbers.append(coeff) 

200 else: 

201 is_complex = pure_complex(coeff) 

202 if is_complex is not None: 

203 complexes = True 

204 x, y = is_complex 

205 if x.is_Rational and y.is_Rational: 

206 if not (x.is_Integer and y.is_Integer): 

207 rationals = True 

208 else: 

209 floats = True 

210 if x.is_Float: 

211 float_numbers.append(x) 

212 if y.is_Float: 

213 float_numbers.append(y) 

214 

215 max_prec = max(c._prec for c in float_numbers) if float_numbers else 53 

216 

217 if floats and complexes: 

218 ground = ComplexField(prec=max_prec) 

219 elif floats: 

220 ground = RealField(prec=max_prec) 

221 elif complexes: 

222 if rationals: 

223 ground = QQ_I 

224 else: 

225 ground = ZZ_I 

226 elif rationals: 

227 ground = QQ 

228 else: 

229 ground = ZZ 

230 

231 result = [] 

232 

233 if not fractions: 

234 domain = ground.poly_ring(*gens) 

235 

236 for numer in numers: 

237 for monom, coeff in numer.items(): 

238 numer[monom] = ground.from_sympy(coeff) 

239 

240 result.append(domain(numer)) 

241 else: 

242 domain = ground.frac_field(*gens) 

243 

244 for numer, denom in zip(numers, denoms): 

245 for monom, coeff in numer.items(): 

246 numer[monom] = ground.from_sympy(coeff) 

247 

248 for monom, coeff in denom.items(): 

249 denom[monom] = ground.from_sympy(coeff) 

250 

251 result.append(domain((numer, denom))) 

252 

253 return domain, result 

254 

255 

256def _construct_expression(coeffs, opt): 

257 """The last resort case, i.e. use the expression domain. """ 

258 domain, result = EX, [] 

259 

260 for coeff in coeffs: 

261 result.append(domain.from_sympy(coeff)) 

262 

263 return domain, result 

264 

265 

266@public 

267def construct_domain(obj, **args): 

268 """Construct a minimal domain for a list of expressions. 

269 

270 Explanation 

271 =========== 

272 

273 Given a list of normal SymPy expressions (of type :py:class:`~.Expr`) 

274 ``construct_domain`` will find a minimal :py:class:`~.Domain` that can 

275 represent those expressions. The expressions will be converted to elements 

276 of the domain and both the domain and the domain elements are returned. 

277 

278 Parameters 

279 ========== 

280 

281 obj: list or dict 

282 The expressions to build a domain for. 

283 

284 **args: keyword arguments 

285 Options that affect the choice of domain. 

286 

287 Returns 

288 ======= 

289 

290 (K, elements): Domain and list of domain elements 

291 The domain K that can represent the expressions and the list or dict 

292 of domain elements representing the same expressions as elements of K. 

293 

294 Examples 

295 ======== 

296 

297 Given a list of :py:class:`~.Integer` ``construct_domain`` will return the 

298 domain :ref:`ZZ` and a list of integers as elements of :ref:`ZZ`. 

299 

300 >>> from sympy import construct_domain, S 

301 >>> expressions = [S(2), S(3), S(4)] 

302 >>> K, elements = construct_domain(expressions) 

303 >>> K 

304 ZZ 

305 >>> elements 

306 [2, 3, 4] 

307 >>> type(elements[0]) # doctest: +SKIP 

308 <class 'int'> 

309 >>> type(expressions[0]) 

310 <class 'sympy.core.numbers.Integer'> 

311 

312 If there are any :py:class:`~.Rational` then :ref:`QQ` is returned 

313 instead. 

314 

315 >>> construct_domain([S(1)/2, S(3)/4]) 

316 (QQ, [1/2, 3/4]) 

317 

318 If there are symbols then a polynomial ring :ref:`K[x]` is returned. 

319 

320 >>> from sympy import symbols 

321 >>> x, y = symbols('x, y') 

322 >>> construct_domain([2*x + 1, S(3)/4]) 

323 (QQ[x], [2*x + 1, 3/4]) 

324 >>> construct_domain([2*x + 1, y]) 

325 (ZZ[x,y], [2*x + 1, y]) 

326 

327 If any symbols appear with negative powers then a rational function field 

328 :ref:`K(x)` will be returned. 

329 

330 >>> construct_domain([y/x, x/(1 - y)]) 

331 (ZZ(x,y), [y/x, -x/(y - 1)]) 

332 

333 Irrational algebraic numbers will result in the :ref:`EX` domain by 

334 default. The keyword argument ``extension=True`` leads to the construction 

335 of an algebraic number field :ref:`QQ(a)`. 

336 

337 >>> from sympy import sqrt 

338 >>> construct_domain([sqrt(2)]) 

339 (EX, [EX(sqrt(2))]) 

340 >>> construct_domain([sqrt(2)], extension=True) # doctest: +SKIP 

341 (QQ<sqrt(2)>, [ANP([1, 0], [1, 0, -2], QQ)]) 

342 

343 See also 

344 ======== 

345 

346 Domain 

347 Expr 

348 """ 

349 opt = build_options(args) 

350 

351 if hasattr(obj, '__iter__'): 

352 if isinstance(obj, dict): 

353 if not obj: 

354 monoms, coeffs = [], [] 

355 else: 

356 monoms, coeffs = list(zip(*list(obj.items()))) 

357 else: 

358 coeffs = obj 

359 else: 

360 coeffs = [obj] 

361 

362 coeffs = list(map(sympify, coeffs)) 

363 result = _construct_simple(coeffs, opt) 

364 

365 if result is not None: 

366 if result is not False: 

367 domain, coeffs = result 

368 else: 

369 domain, coeffs = _construct_expression(coeffs, opt) 

370 else: 

371 if opt.composite is False: 

372 result = None 

373 else: 

374 result = _construct_composite(coeffs, opt) 

375 

376 if result is not None: 

377 domain, coeffs = result 

378 else: 

379 domain, coeffs = _construct_expression(coeffs, opt) 

380 

381 if hasattr(obj, '__iter__'): 

382 if isinstance(obj, dict): 

383 return domain, dict(list(zip(monoms, coeffs))) 

384 else: 

385 return domain, coeffs 

386 else: 

387 return domain, coeffs[0]