Repository: sympy/sympy

Issue:
linsolve gives odd result if symbols are duplicated
```python
>>> linsolve([x-1],x)
{(1,)}
>>> linsolve([x-1],x,y)
{(1, y)}
>>> linsolve([x-1],x,x)
{(x + 1, x + 1)}  <---------- ?
```

Discussion:
sympy-bot: :white_check_mark:

Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot) (v167). I'm here to help you write a release notes entry. Please read the [guide on how to write release notes](https://github.com/sympy/sympy/wiki/Writing-Release-Notes).



Your release notes are in good order.

Here is what the release notes will look like:
* solvers
  * `linsolve` raises ValueError if duplicate symbols are detected (previously the wrong result was returned) ([#23872](https://github.com/sympy/sympy/pull/23872) by [@smichr](https://github.com/smichr))

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.12.

<details><summary>Click here to see the pull request description that was parsed.</summary>

    <!-- Your title above should be a short description of what
    was changed. Do not include the issue number in the title. -->

    #### References to other Issues or PRs
    <!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
    format, e.g. "Fixes #1234" (see
    https://tinyurl.com/auto-closing for more information). Also, please
    write a comment on that issue linking back to this pull request once it is
    open. -->

    fixes #23855 

    #### Brief description of what is fixed or changed

    It would be nice if someone took this as a first issue...but it is important not to let the wrong result be returned for so simple a case.
    #### Other comments


    #### Release Notes

    <!-- Write the release notes for this release below between the BEGIN and END
    statements. The basic format is a bulleted list with the name of the subpackage
    and the release note for this PR. For example:

    * solvers
      * Added a new solver for logarithmic equations.

    * functions
      * Fixed a bug with log of integers.

    or if no release note(s) should be included use:

    NO ENTRY

    See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
    information on how to write release notes. The bot will check your release
    notes automatically to see if they are formatted correctly. -->

    <!-- BEGIN RELEASE NOTES -->
    * solvers
      * `linsolve` raises ValueError if duplicate symbols are detected (previously the wrong result was returned)
    <!-- END RELEASE NOTES -->


</details><p>

github-actions[bot]: Benchmark results from GitHub Actions

Lower numbers are good, higher numbers are bad. A ratio less than 1
means a speed up and greater than 1 means a slowdown. Green lines
beginning with `+` are slowdowns (the PR is slower then master or
master is slower than the previous release). Red lines beginning
with `-` are speedups.

Significantly changed benchmark results (PR vs master)
```diff

```
Significantly changed benchmark results (master vs previous release)
```diff

```
Full benchmark results can be found as artifacts in GitHub Actions
(click on checks at the top of the PR).

Patch:
diff --git a/.mailmap b/.mailmap
index 071f9cf1d5..e6d6f4726f 100644
--- a/.mailmap
+++ b/.mailmap
@@ -743,8 +743,6 @@ Ka Yi <chua.kayi@yahoo.com.sg>
 Kaifeng Zhu <cafeeee@gmail.com>
 Kalevi Suominen <jksuom@gmail.com> <kalevi.suominen@helsinki.fi>
 Kamlesh Joshi <72374645+kamleshjoshi8102@users.noreply.github.com>
-Karan <grgkaran03@gmail.com>
-Karan <grgkaran03@gmail.com>
 Karan Sharma <karan1276@gmail.com>
 Karthik Chintapalli <karthik.chintapalli@students.iiit.ac.in> <karthik.chintapalli@gmail.com>
 Kartik Sethi <kartiks31416@gmail.com> Kartik Sethi <37146279+ks147@users.noreply.github.com>
diff --git a/doc/src/modules/utilities/iterables.rst b/doc/src/modules/utilities/iterables.rst
index 9c04a38f04..e851b35e3b 100644
--- a/doc/src/modules/utilities/iterables.rst
+++ b/doc/src/modules/utilities/iterables.rst
@@ -29,15 +29,7 @@ functions to generate partitions that can be used as low-level tools for
 routines:  ``partitions`` and ``multiset_partitions``. The former gives
 integer partitions, and the latter gives enumerated partitions of elements.
 There is also a routine ``kbins`` that will give a variety of permutations
-of partions. And to obtain partitions as a list instead of a dictionary, there
-is the ``ordered_partition`` function which is quite fast. Finally, to simply
-obtain a count of the number of partitions without enumerating them, there
-is the ``nT`` function.
-
-See Also
-========
-
-sympy.utilities.iterables.ordered_partitions, sympy.functions.combinatorial.numbers.nT
+of partions.
 
 partitions::
 
diff --git a/sympy/physics/continuum_mechanics/truss.py b/sympy/physics/continuum_mechanics/truss.py
index 5c2d0d0517..f4df483bdb 100644
--- a/sympy/physics/continuum_mechanics/truss.py
+++ b/sympy/physics/continuum_mechanics/truss.py
@@ -3,15 +3,8 @@
 to 2D Trusses.
 """
 
-from cmath import inf
-from sympy.core.add import Add
-from sympy.core.mul import Mul
 from sympy.core.symbol import Symbol
 from sympy.core.sympify import sympify
-from sympy import Matrix, pi
-from sympy.functions.elementary.miscellaneous import sqrt
-from sympy.matrices.dense import zeros
-import math
 
 
 
@@ -66,7 +59,6 @@ def __init__(self):
         self._nodes_occupied = {}
         self._reaction_loads = {}
         self._internal_forces = {}
-        self._node_coordinates = {}
 
     @property
     def nodes(self):
@@ -174,7 +166,6 @@ def add_node(self, label, x, y):
             self._node_positions.append((x, y))
             self._node_position_x.append(x)
             self._node_position_y.append(y)
-            self._node_coordinates[label] = [x, y]
 
     def remove_node(self, label):
         """
@@ -222,7 +213,6 @@ def remove_node(self, label):
                 self._loads.pop(label)
             if label in list(self._supports):
                 self._supports.pop(label)
-            self._node_coordinates.pop(label)
 
     def add_member(self, label, start, end):
         """
@@ -337,8 +327,6 @@ def change_node_label(self, label, new_label):
                 if node[0] == label:
                     self._nodes[self._nodes.index((label, node[1], node[2]))] = (new_label, node[1], node[2])
                     self._node_labels[self._node_labels.index(node[0])] = new_label
-                    self._node_coordinates[new_label] = self._node_coordinates[label]
-                    self._node_coordinates.pop(label)
                     if node[0] in list(self._supports):
                         self._supports[new_label] = self._supports[node[0]]
                         self._supports.pop(node[0])
@@ -611,125 +599,3 @@ def remove_support(self, location):
             elif self._supports[location] == 'roller':
                 self.remove_load(location, Symbol('R_'+str(location)+'_y'), 90)
             self._supports.pop(location)
-
-    def solve(self):
-        """
-        This method solves for all reaction forces of all supports and all internal forces
-        of all the members in the truss, provided the Truss is solvable.
-
-        A Truss is solvable if the following condition is met,
-
-        2n >= r + m
-
-        Where n is the number of nodes, r is the number of reaction forces, where each pinned
-        support has 2 reaction forces and each roller has 1, and m is the number of members.
-
-        The given condition is derived from the fact that a system of equations is solvable
-        only when the number of variables is lesser than or equal to the number of equations.
-        Equilibrium Equations in x and y directions give two equations per node giving 2n number
-        equations. However, the truss needs to be stable as well and may be unstable if 2n > r + m.
-        The number of variables is simply the sum of the number of reaction forces and member
-        forces.
-
-        .. note::
-           The sign convention for the internal forces present in a member revolves around whether each
-           force is compressive or tensile. While forming equations for each node, internal force due
-           to a member on the node is assumed to be away from the node i.e. each force is assumed to
-           be compressive by default. Hence, a positive value for an internal force implies the
-           presence of compressive force in the member and a negative value implies a tensile force.
-
-        Examples
-        ========
-
-        >>> from sympy.physics.continuum_mechanics.truss import Truss
-        >>> t = Truss()
-        >>> t.add_node("node_1", 0, 0)
-        >>> t.add_node("node_2", 6, 0)
-        >>> t.add_node("node_3", 2, 2)
-        >>> t.add_node("node_4", 2, 0)
-        >>> t.add_member("member_1", "node_1", "node_4")
-        >>> t.add_member("member_2", "node_2", "node_4")
-        >>> t.add_member("member_3", "node_1", "node_3")
-        >>> t.add_member("member_4", "node_2", "node_3")
-        >>> t.add_member("member_5", "node_3", "node_4")
-        >>> t.apply_load("node_4", magnitude=10, direction=270)
-        >>> t.apply_support("node_1", type="pinned")
-        >>> t.apply_support("node_2", type="roller")
-        >>> t.solve()
-        >>> t.reaction_loads
-        {'R_node_1_x': 0, 'R_node_1_y': 6.66666666666667, 'R_node_2_y': 3.33333333333333}
-        >>> t.internal_forces
-        {'member_1': 6.66666666666666, 'member_2': 6.66666666666667, 'member_3': -6.66666666666667*sqrt(2), 'member_4': -3.33333333333333*sqrt(5), 'member_5': 10.0}
-        """
-        count_reaction_loads = 0
-        for node in self._nodes:
-            if node[0] in list(self._supports):
-                if self._supports[node[0]]=='pinned':
-                    count_reaction_loads += 2
-                elif self._supports[node[0]]=='roller':
-                    count_reaction_loads += 1
-        if 2*len(self._nodes) != len(self._members) + count_reaction_loads:
-            raise ValueError("The given truss cannot be solved")
-        coefficients_matrix = [[0 for i in range(2*len(self._nodes))] for j in range(2*len(self._nodes))]
-        load_matrix = zeros(2*len(self.nodes), 1)
-        load_matrix_row = 0
-        for node in self._nodes:
-            if node[0] in list(self._loads):
-                for load in self._loads[node[0]]:
-                    if load[0]!=Symbol('R_'+str(node[0])+'_x') and load[0]!=Symbol('R_'+str(node[0])+'_y'):
-                        load_matrix[load_matrix_row] -= load[0]*math.cos(pi*load[1]/180)
-                        load_matrix[load_matrix_row + 1] -= load[0]*math.sin(pi*load[1]/180)
-            load_matrix_row += 2
-        cols = 0
-        row = 0
-        for node in self._nodes:
-            if node[0] in list(self._supports):
-                if self._supports[node[0]]=='pinned':
-                    coefficients_matrix[row][cols] += 1
-                    coefficients_matrix[row+1][cols+1] += 1
-                    cols += 2
-                elif self._supports[node[0]]=='roller':
-                    coefficients_matrix[row+1][cols] += 1
-                    cols += 1
-            row += 2
-        for member in list(self._members):
-            start = self._members[member][0]
-            end = self._members[member][1]
-            length = sqrt((self._node_coordinates[start][0]-self._node_coordinates[end][0])**2 + (self._node_coordinates[start][1]-self._node_coordinates[end][1])**2)
-            start_index = self._node_labels.index(start)
-            end_index = self._node_labels.index(end)
-            horizontal_component_start = (self._node_coordinates[end][0]-self._node_coordinates[start][0])/length
-            vertical_component_start = (self._node_coordinates[end][1]-self._node_coordinates[start][1])/length
-            horizontal_component_end = (self._node_coordinates[start][0]-self._node_coordinates[end][0])/length
-            vertical_component_end = (self._node_coordinates[start][1]-self._node_coordinates[end][1])/length
-            coefficients_matrix[start_index*2][cols] += horizontal_component_start
-            coefficients_matrix[start_index*2+1][cols] += vertical_component_start
-            coefficients_matrix[end_index*2][cols] += horizontal_component_end
-            coefficients_matrix[end_index*2+1][cols] += vertical_component_end
-            cols += 1
-        forces_matrix = (Matrix(coefficients_matrix)**-1)*load_matrix
-        self._reaction_loads = {}
-        i = 0
-        min_load = inf
-        for node in self._nodes:
-            if node[0] in list(self._loads):
-                for load in self._loads[node[0]]:
-                    if type(load[0]) not in [Symbol, Mul, Add]:
-                        min_load = min(min_load, load[0])
-        for j in range(len(forces_matrix)):
-            if type(forces_matrix[j]) not in [Symbol, Mul, Add]:
-                if abs(forces_matrix[j]/min_load) <1E-10:
-                    forces_matrix[j] = 0
-        for node in self._nodes:
-            if node[0] in list(self._supports):
-                if self._supports[node[0]]=='pinned':
-                    self._reaction_loads['R_'+str(node[0])+'_x'] = forces_matrix[i]
-                    self._reaction_loads['R_'+str(node[0])+'_y'] = forces_matrix[i+1]
-                    i += 2
-                elif self._supports[node[0]]=='roller':
-                    self._reaction_loads['R_'+str(node[0])+'_y'] = forces_matrix[i]
-                    i += 1
-        for member in list(self._members):
-            self._internal_forces[member] = forces_matrix[i]
-            i += 1
-        return
diff --git a/sympy/solvers/solveset.py b/sympy/solvers/solveset.py
index b0c49825a9..59c681c302 100644
--- a/sympy/solvers/solveset.py
+++ b/sympy/solvers/solveset.py
@@ -2778,6 +2778,8 @@ def linsolve(system, *symbols):
     if symbols and hasattr(symbols[0], '__iter__'):
         symbols = symbols[0]
     sym_gen = isinstance(symbols, GeneratorType)
+    dup_msg = 'duplicate symbols given'
+
 
     b = None  # if we don't get b the input was bad
     # unpack system
@@ -2796,6 +2798,8 @@ def linsolve(system, *symbols):
                     symbols for which a solution is being sought must
                     be given as a sequence, too.
                 '''))
+            if len(set(symbols)) != len(symbols):
+                raise ValueError(dup_msg)
 
             #
             # Pass to the sparse solver implemented in polys. It is important
@@ -2827,13 +2831,16 @@ def linsolve(system, *symbols):
         raise ValueError("Invalid arguments")
     if sym_gen:
         symbols = [next(symbols) for i in range(A.cols)]
-        if any(set(symbols) & (A.free_symbols | b.free_symbols)):
+        symset = set(symbols)
+        if any(symset & (A.free_symbols | b.free_symbols)):
             raise ValueError(filldedent('''
                 At least one of the symbols provided
                 already appears in the system to be solved.
                 One way to avoid this is to use Dummy symbols in
                 the generator, e.g. numbered_symbols('%s', cls=Dummy)
             ''' % symbols[0].name.rstrip('1234567890')))
+        elif len(symset) != len(symbols):
+            raise ValueError(dup_msg)
 
     if not symbols:
         symbols = [Dummy() for _ in range(A.cols)]


Test patch:
diff --git a/sympy/physics/continuum_mechanics/tests/test_truss.py b/sympy/physics/continuum_mechanics/tests/test_truss.py
index addeaa46fb..8fbd9fc9b1 100644
--- a/sympy/physics/continuum_mechanics/tests/test_truss.py
+++ b/sympy/physics/continuum_mechanics/tests/test_truss.py
@@ -1,7 +1,6 @@
 from sympy.core.symbol import Symbol, symbols
 from sympy.physics.continuum_mechanics.truss import Truss
 
-
 def test_truss():
     A = Symbol('A')
     B = Symbol('B')
@@ -94,15 +93,3 @@ def test_truss():
     assert t.supports == {D: 'roller'}
     assert t.reaction_loads == {}
     assert t.loads == {A: [[P, 90], [2*P, 45]], D: [[P/2, 90], [Symbol('R_D_y'), 90]]}
-
-    t.apply_support(A, "pinned")
-
-    # testing the solve method
-    t.solve()
-    assert t.reaction_loads['R_A_x']/P - (-1.4142135623731) < 1e-10
-    assert t.reaction_loads['R_A_y']/P - (-2.41421356237309) < 1e-10
-    assert t.reaction_loads['R_D_y']/P - (-0.5) < 1e-10
-    assert t.internal_forces[AB]/P < 1e-10
-    assert t.internal_forces[CD] < 1e-10
-    assert t.internal_forces[AC] < 1e-10
-    # assert t.internal_forces == {AB: 3.06161699786838e-17*P, CD: 0, AC: 0}
diff --git a/sympy/solvers/tests/test_solveset.py b/sympy/solvers/tests/test_solveset.py
index 5895a358f1..2e39875033 100644
--- a/sympy/solvers/tests/test_solveset.py
+++ b/sympy/solvers/tests/test_solveset.py
@@ -1437,6 +1437,8 @@ def test_linsolve():
     raises(ValueError, lambda: linsolve(x1, x2))
     raises(ValueError, lambda: linsolve((A,), x1, x2))
     raises(ValueError, lambda: linsolve(A, B, x1, x2))
+    raises(ValueError, lambda: linsolve([x1], x1, x1))
+    raises(ValueError, lambda: linsolve([x1], (i for i in (x1, x1))))
 
     #raise ValueError if equations are non-linear in given variables
     raises(NonlinearError, lambda: linsolve([x + y - 1, x ** 2 + y - 3], [x, y]))