Repository: sympy/sympy
Pull request: Reject unordered sets in linear_eq_to_matrix
#### References to other Issues or PRs

Fixes https://github.com/sympy/sympy/pull/25812
This PR contains the squashed commit version of the previously closed #25812 . This new PR do not have commits which will unintentionally affect a large number of lines in the blame.

Fixes https://github.com/sympy/sympy/issues/25423
If a user relies on `sets` to pass symbols to `linear_eq_to_matrix`, they might receive results that appear inconsistent. Since the equations' coefficients matrix depends on the order of symbols, using an unordered set can lead to confusion. Different runs could potentially produce different matrices, even if the equations and symbols remain the same.

#### Brief description of what is fixed or changed

Previous implementations allowed unordered `sets` as inputs for `linear_eq_to_matrix`, leading to unpredictable outputs. With input validation in `solveset.py` , symbols are provided in a predictable sequence, enforcing consistent behavior.

New test cases implemented in `test_solveset.py`cover the scenarios where `sets` are passed to the function, ensuring proper error responses.

#### Release Notes

<!-- BEGIN RELEASE NOTES -->
* solvers
  * `linear_eq_to_matrix` now raises TypeError when passing unordered symbols
<!-- END RELEASE NOTES -->


Problem statement:
`linear_eq_to_matrix` should reject unordered symbols
Although dictionaries are ordered in newer Python versions, sets are unordered and should be rejected as input to `linear_eq_to_matrix` which returns the linear coefficients for one or more expressions *in the order that symbols are given*. Although the routine which this function uses will order the symbols, this order is not reported back to the user. It might be better to simply raise an error in this case.
```python
>>> linear_eq_to_matrix(2*x + y, {x, y})  # order is `list(ordered({x,y})) -> [x, y]
(Matrix([[2, 1]]), Matrix([[0]]))
```

Hints from issue discussion:
Can I work on this?

Commit message: Reject unordered sets in linear_eq_to_matrix

Previous implementations allowed unordered sets as inputs for
linear_eq_to_matrix, leading to unpredictable outputs. This change
introduces input validation to ensure that the symbols are provided
in a predictable sequence, enforcing consistent behavior.

New test cases cover the scenarios where sets are passed to the
function, ensuring proper error responses.

Resolves: #issue-number (if applicable)
author: add Congxu Yang to .mailmap

Patch to review:
diff --git a/.mailmap b/.mailmap
index 80e50bcc92c1..512b3fd19773 100644
--- a/.mailmap
+++ b/.mailmap
@@ -466,6 +466,7 @@ Colin B. Macdonald <cbm@m.fsf.org> <macdonald@maths.ox.ac.uk>
 Colin Marquardt <github@marquardt-home.de>
 Colleen Lee <colleenclee@gmail.com> <clee@coursera.org>
 Comer Duncan <comer.duncan@gmail.com>
+Congxu Yang <u7189828@anu.edu.au>
 Constantin Mateescu <costica1234@me.com>
 Costor <pcs2009@web.de>
 Craig A. Stoudt <craig.stoudt@gmail.com>
diff --git a/sympy/solvers/solveset.py b/sympy/solvers/solveset.py
index 0f0d947d114e..f5650e111631 100644
--- a/sympy/solvers/solveset.py
+++ b/sympy/solvers/solveset.py
@@ -2605,6 +2605,11 @@ def linear_eq_to_matrix(equations, *symbols):
             are to be found.
             '''))
 
+    # Check if 'symbols' is a set and raise an error if it is
+    if isinstance(symbols[0], set):
+        raise TypeError(
+            "Unordered 'set' type is not supported as input for symbols.")
+
     if hasattr(symbols[0], '__iter__'):
         symbols = symbols[0]
 
diff --git a/sympy/solvers/tests/test_solveset.py b/sympy/solvers/tests/test_solveset.py
index 7b4473fd5a70..c100c19cd60a 100644
--- a/sympy/solvers/tests/test_solveset.py
+++ b/sympy/solvers/tests/test_solveset.py
@@ -3271,3 +3271,13 @@ def test_issue_22628():
 
 def test_issue_25781():
     assert solve(sqrt(x/2) - x) == [0, S.Half]
+
+
+def test_issue_25423():
+    x, y = symbols('x y')
+    equations1 = []
+    equations2 = [2 * x + 3 * y - 1]
+    nonlinear_equations = [x ** 2 + y ** 2 - 1, x - y]
+    raises(TypeError, lambda: linear_eq_to_matrix(equations1, {x, y}))
+    raises(TypeError, lambda: linear_eq_to_matrix(equations2, {x, y}))
+    raises(NonlinearError, lambda: linear_eq_to_matrix(nonlinear_equations, x, y))


Code review comments:
diff_hunk: @@ -3271,3 +3271,13 @@ def test_issue_22628():
 
 def test_issue_25781():
     assert solve(sqrt(x/2) - x) == [0, S.Half]
+
+
+def test_issue_25423():
+    x, y = symbols('x y')
+    equations1 = []
+    equations2 = [2 * x + 3 * y - 1]
+    nonlinear_equations = [x ** 2 + y ** 2 - 1, x - y]
+    raises(TypeError, lambda: linear_eq_to_matrix(equations1, {x, y}))
+    raises(TypeError, lambda: linear_eq_to_matrix(equations2, {x, y}))
+    raises(NonlinearError, lambda: linear_eq_to_matrix(nonlinear_equations, x, y))
path: sympy/solvers/tests/test_solveset.py
text: @author:
This change from TypeError to ValueError will cause test failure becasue within the `linear_eq_to_matrix` function, a _TypeError_ is raised when checking to see if the symbols parameter is a set, indicating that an unordered `set` type is not supported for the symbols parameter. This is the actual behavior of the function checking the `set` input. 

I think since we are concerned with `set` type as input, we should indeed raise _TypeError_ instead of ValueError?

@user2:
For some reason I think the preference is to raise a ValueError.  @user1 ?

That would be consistent with raising a ValueError when a set is used to pass the equations, too.

diff_hunk: @@ -3271,3 +3271,13 @@ def test_issue_22628():
 
 def test_issue_25781():
     assert solve(sqrt(x/2) - x) == [0, S.Half]
+
+
+def test_issue_25423():
+    x, y = symbols('x y')
+    equations1 = []
+    equations2 = [2 * x + 3 * y - 1]
+    nonlinear_equations = [x ** 2 + y ** 2 - 1, x - y]
+    raises(TypeError, lambda: linear_eq_to_matrix(equations1, {x, y}))
+    raises(TypeError, lambda: linear_eq_to_matrix(equations2, {x, y}))
+    raises(NonlinearError, lambda: linear_eq_to_matrix(nonlinear_equations, x, y))
path: sympy/solvers/tests/test_solveset.py
text: @user1:
Instead of this, how about showing/affirming that passing `set(equations2)` with `[x, y]` does *not* raise an error.

Also, symbols `x` and `y` have already been created at the top of the file and need not be recreated here.

@author:
In the case of `set(equations2)`, there will be test failure indicates that the current implementation of `linear_eq_to_matrix` does not support a set of equations as input. It will raise exception becasue Python set is not recognized as a valid sequence for this purpose. 

In this case, for the test to work, I will need to convert `sets` to `list`. In such a case, we are not testing the function's ability to handle sets directly. It's a gap in our testing if the function should handle sets.



@user1:
OK, that's fine (and probably better) that an error raises. It won't change the solution but it could be a little harder to debug if the order changes wrt what was passed. Then let's delete that 3rd test since it is already tested in lines 1408ff in the same file.

diff_hunk: @@ -3271,3 +3271,13 @@ def test_issue_22628():
 
 def test_issue_25781():
     assert solve(sqrt(x/2) - x) == [0, S.Half]
+
+
+def test_issue_25423():
+    x, y = symbols('x y')
+    equations1 = []
+    equations2 = [2 * x + 3 * y - 1]
+    nonlinear_equations = [x ** 2 + y ** 2 - 1, x - y]
+    raises(TypeError, lambda: linear_eq_to_matrix(equations1, {x, y}))
+    raises(TypeError, lambda: linear_eq_to_matrix(equations2, {x, y}))
+    raises(NonlinearError, lambda: linear_eq_to_matrix(nonlinear_equations, x, y))
path: sympy/solvers/tests/test_solveset.py
text: @user1:
```suggestion
    raises(TypeError, lambda: linear_eq_to_matrix(equations1, {x, y}))
    raises(TypeError, lambda: linear_eq_to_matrix(equations2, {x, y}))
    raises(ValueError, lambda: linear_eq_to_matrix(set(equations2), (x, y)))
```