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
« 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
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
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 = []
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
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
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
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
76 result = [domain.from_sympy(coeff) for coeff in coeffs]
78 return domain, result
81def _construct_algebraic(coeffs, opt):
82 """We know that coefficients are algebraic so construct the extension. """
83 from sympy.polys.numberfields import primitive_element
85 exts = set()
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
102 trees = build_trees(coeffs)
103 exts = list(ordered(exts))
105 g, span, H = primitive_element(exts, ex=True, polys=True)
106 root = sum([ s*ext for s, ext in zip(span, exts) ])
108 domain, g = QQ.algebraic_field((g, root)), g.rep.rep
110 exts_dom = [domain.dtype.from_list(h, g, QQ) for h in H]
111 exts_map = dict(zip(exts, exts_dom))
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
126 result = [convert_tree(tree) for tree in trees]
128 return domain, result
131def _construct_composite(coeffs, opt):
132 """Handle composite domains, e.g.: ZZ[X], QQ[X], ZZ(X), QQ(X). """
133 numers, denoms = [], []
135 for coeff in coeffs:
136 numer, denom = coeff.as_numer_denom()
138 numers.append(numer)
139 denoms.append(denom)
141 polys, gens = parallel_dict_from_basic(numers + denoms) # XXX: sorting
142 if not gens:
143 return None
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
149 all_symbols = set()
151 for gen in gens:
152 symbols = gen.free_symbols
154 if all_symbols & symbols:
155 return None # there could be algebraic relations between generators
156 else:
157 all_symbols |= symbols
159 n = len(gens)
160 k = len(polys)//2
162 numers = polys[:k]
163 denoms = polys[k:]
165 if opt.field:
166 fractions = True
167 else:
168 fractions, zeros = False, (0,)*n
170 for denom in denoms:
171 if len(denom) > 1 or zeros not in denom:
172 fractions = True
173 break
175 coeffs = set()
177 if not fractions:
178 for numer, denom in zip(numers, denoms):
179 denom = denom[zeros]
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()))
190 rationals = floats = complexes = False
191 float_numbers = []
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)
215 max_prec = max(c._prec for c in float_numbers) if float_numbers else 53
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
231 result = []
233 if not fractions:
234 domain = ground.poly_ring(*gens)
236 for numer in numers:
237 for monom, coeff in numer.items():
238 numer[monom] = ground.from_sympy(coeff)
240 result.append(domain(numer))
241 else:
242 domain = ground.frac_field(*gens)
244 for numer, denom in zip(numers, denoms):
245 for monom, coeff in numer.items():
246 numer[monom] = ground.from_sympy(coeff)
248 for monom, coeff in denom.items():
249 denom[monom] = ground.from_sympy(coeff)
251 result.append(domain((numer, denom)))
253 return domain, result
256def _construct_expression(coeffs, opt):
257 """The last resort case, i.e. use the expression domain. """
258 domain, result = EX, []
260 for coeff in coeffs:
261 result.append(domain.from_sympy(coeff))
263 return domain, result
266@public
267def construct_domain(obj, **args):
268 """Construct a minimal domain for a list of expressions.
270 Explanation
271 ===========
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.
278 Parameters
279 ==========
281 obj: list or dict
282 The expressions to build a domain for.
284 **args: keyword arguments
285 Options that affect the choice of domain.
287 Returns
288 =======
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.
294 Examples
295 ========
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`.
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'>
312 If there are any :py:class:`~.Rational` then :ref:`QQ` is returned
313 instead.
315 >>> construct_domain([S(1)/2, S(3)/4])
316 (QQ, [1/2, 3/4])
318 If there are symbols then a polynomial ring :ref:`K[x]` is returned.
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])
327 If any symbols appear with negative powers then a rational function field
328 :ref:`K(x)` will be returned.
330 >>> construct_domain([y/x, x/(1 - y)])
331 (ZZ(x,y), [y/x, -x/(y - 1)])
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)`.
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)])
343 See also
344 ========
346 Domain
347 Expr
348 """
349 opt = build_options(args)
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]
362 coeffs = list(map(sympify, coeffs))
363 result = _construct_simple(coeffs, opt)
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)
376 if result is not None:
377 domain, coeffs = result
378 else:
379 domain, coeffs = _construct_expression(coeffs, opt)
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]