Coverage for /usr/lib/python3/dist-packages/sympy/polys/matrices/linsolve.py: 12%
100 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#
2# sympy.polys.matrices.linsolve module
3#
4# This module defines the _linsolve function which is the internal workhorse
5# used by linsolve. This computes the solution of a system of linear equations
6# using the SDM sparse matrix implementation in sympy.polys.matrices.sdm. This
7# is a replacement for solve_lin_sys in sympy.polys.solvers which is
8# inefficient for large sparse systems due to the use of a PolyRing with many
9# generators:
10#
11# https://github.com/sympy/sympy/issues/20857
12#
13# The implementation of _linsolve here handles:
14#
15# - Extracting the coefficients from the Expr/Eq input equations.
16# - Constructing a domain and converting the coefficients to
17# that domain.
18# - Using the SDM.rref, SDM.nullspace etc methods to generate the full
19# solution working with arithmetic only in the domain of the coefficients.
20#
21# The routines here are particularly designed to be efficient for large sparse
22# systems of linear equations although as well as dense systems. It is
23# possible that for some small dense systems solve_lin_sys which uses the
24# dense matrix implementation DDM will be more efficient. With smaller systems
25# though the bulk of the time is spent just preprocessing the inputs and the
26# relative time spent in rref is too small to be noticeable.
27#
29from collections import defaultdict
31from sympy.core.add import Add
32from sympy.core.mul import Mul
33from sympy.core.singleton import S
35from sympy.polys.constructor import construct_domain
36from sympy.polys.solvers import PolyNonlinearError
38from .sdm import (
39 SDM,
40 sdm_irref,
41 sdm_particular_from_rref,
42 sdm_nullspace_from_rref
43)
45from sympy.utilities.misc import filldedent
48def _linsolve(eqs, syms):
50 """Solve a linear system of equations.
52 Examples
53 ========
55 Solve a linear system with a unique solution:
57 >>> from sympy import symbols, Eq
58 >>> from sympy.polys.matrices.linsolve import _linsolve
59 >>> x, y = symbols('x, y')
60 >>> eqs = [Eq(x + y, 1), Eq(x - y, 2)]
61 >>> _linsolve(eqs, [x, y])
62 {x: 3/2, y: -1/2}
64 In the case of underdetermined systems the solution will be expressed in
65 terms of the unknown symbols that are unconstrained:
67 >>> _linsolve([Eq(x + y, 0)], [x, y])
68 {x: -y, y: y}
70 """
71 # Number of unknowns (columns in the non-augmented matrix)
72 nsyms = len(syms)
74 # Convert to sparse augmented matrix (len(eqs) x (nsyms+1))
75 eqsdict, const = _linear_eq_to_dict(eqs, syms)
76 Aaug = sympy_dict_to_dm(eqsdict, const, syms)
77 K = Aaug.domain
79 # sdm_irref has issues with float matrices. This uses the ddm_rref()
80 # function. When sdm_rref() can handle float matrices reasonably this
81 # should be removed...
82 if K.is_RealField or K.is_ComplexField:
83 Aaug = Aaug.to_ddm().rref()[0].to_sdm()
85 # Compute reduced-row echelon form (RREF)
86 Arref, pivots, nzcols = sdm_irref(Aaug)
88 # No solution:
89 if pivots and pivots[-1] == nsyms:
90 return None
92 # Particular solution for non-homogeneous system:
93 P = sdm_particular_from_rref(Arref, nsyms+1, pivots)
95 # Nullspace - general solution to homogeneous system
96 # Note: using nsyms not nsyms+1 to ignore last column
97 V, nonpivots = sdm_nullspace_from_rref(Arref, K.one, nsyms, pivots, nzcols)
99 # Collect together terms from particular and nullspace:
100 sol = defaultdict(list)
101 for i, v in P.items():
102 sol[syms[i]].append(K.to_sympy(v))
103 for npi, Vi in zip(nonpivots, V):
104 sym = syms[npi]
105 for i, v in Vi.items():
106 sol[syms[i]].append(sym * K.to_sympy(v))
108 # Use a single call to Add for each term:
109 sol = {s: Add(*terms) for s, terms in sol.items()}
111 # Fill in the zeros:
112 zero = S.Zero
113 for s in set(syms) - set(sol):
114 sol[s] = zero
116 # All done!
117 return sol
120def sympy_dict_to_dm(eqs_coeffs, eqs_rhs, syms):
121 """Convert a system of dict equations to a sparse augmented matrix"""
122 elems = set(eqs_rhs).union(*(e.values() for e in eqs_coeffs))
123 K, elems_K = construct_domain(elems, field=True, extension=True)
124 elem_map = dict(zip(elems, elems_K))
125 neqs = len(eqs_coeffs)
126 nsyms = len(syms)
127 sym2index = dict(zip(syms, range(nsyms)))
128 eqsdict = []
129 for eq, rhs in zip(eqs_coeffs, eqs_rhs):
130 eqdict = {sym2index[s]: elem_map[c] for s, c in eq.items()}
131 if rhs:
132 eqdict[nsyms] = -elem_map[rhs]
133 if eqdict:
134 eqsdict.append(eqdict)
135 sdm_aug = SDM(enumerate(eqsdict), (neqs, nsyms + 1), K)
136 return sdm_aug
139def _linear_eq_to_dict(eqs, syms):
140 """Convert a system Expr/Eq equations into dict form, returning
141 the coefficient dictionaries and a list of syms-independent terms
142 from each expression in ``eqs```.
144 Examples
145 ========
147 >>> from sympy.polys.matrices.linsolve import _linear_eq_to_dict
148 >>> from sympy.abc import x
149 >>> _linear_eq_to_dict([2*x + 3], {x})
150 ([{x: 2}], [3])
151 """
152 coeffs = []
153 ind = []
154 symset = set(syms)
155 for i, e in enumerate(eqs):
156 if e.is_Equality:
157 coeff, terms = _lin_eq2dict(e.lhs, symset)
158 cR, tR = _lin_eq2dict(e.rhs, symset)
159 # there were no nonlinear errors so now
160 # cancellation is allowed
161 coeff -= cR
162 for k, v in tR.items():
163 if k in terms:
164 terms[k] -= v
165 else:
166 terms[k] = -v
167 # don't store coefficients of 0, however
168 terms = {k: v for k, v in terms.items() if v}
169 c, d = coeff, terms
170 else:
171 c, d = _lin_eq2dict(e, symset)
172 coeffs.append(d)
173 ind.append(c)
174 return coeffs, ind
177def _lin_eq2dict(a, symset):
178 """return (c, d) where c is the sym-independent part of ``a`` and
179 ``d`` is an efficiently calculated dictionary mapping symbols to
180 their coefficients. A PolyNonlinearError is raised if non-linearity
181 is detected.
183 The values in the dictionary will be non-zero.
185 Examples
186 ========
188 >>> from sympy.polys.matrices.linsolve import _lin_eq2dict
189 >>> from sympy.abc import x, y
190 >>> _lin_eq2dict(x + 2*y + 3, {x, y})
191 (3, {x: 1, y: 2})
192 """
193 if a in symset:
194 return S.Zero, {a: S.One}
195 elif a.is_Add:
196 terms_list = defaultdict(list)
197 coeff_list = []
198 for ai in a.args:
199 ci, ti = _lin_eq2dict(ai, symset)
200 coeff_list.append(ci)
201 for mij, cij in ti.items():
202 terms_list[mij].append(cij)
203 coeff = Add(*coeff_list)
204 terms = {sym: Add(*coeffs) for sym, coeffs in terms_list.items()}
205 return coeff, terms
206 elif a.is_Mul:
207 terms = terms_coeff = None
208 coeff_list = []
209 for ai in a.args:
210 ci, ti = _lin_eq2dict(ai, symset)
211 if not ti:
212 coeff_list.append(ci)
213 elif terms is None:
214 terms = ti
215 terms_coeff = ci
216 else:
217 # since ti is not null and we already have
218 # a term, this is a cross term
219 raise PolyNonlinearError(filldedent('''
220 nonlinear cross-term: %s''' % a))
221 coeff = Mul._from_args(coeff_list)
222 if terms is None:
223 return coeff, {}
224 else:
225 terms = {sym: coeff * c for sym, c in terms.items()}
226 return coeff * terms_coeff, terms
227 elif not a.has_xfree(symset):
228 return a, {}
229 else:
230 raise PolyNonlinearError('nonlinear term: %s' % a)