Repository: sympy/sympy
Pull request: vector: created VectorKind and Add with vectors now calls VectorAdd
#### References to other Issues or PRs

Fixes #26121

#### Brief description of what is fixed or changed

Created a VectorKind class, which is the kind for all vector objects in Sympy. The result of operations between vectors, such as Cross, also has VectorKind.
Added a postprocessor for Add with respect to Vector class to make sure that when calling Add with vectors as arguments the result is an instance of VectorAdd.
Added tests to verify all this.

Example:

In [1]: Sys = CoordSys3D('Sys')

In [2]: Sys.i.kind

Out [2]: VectorKind

In [3]: isinstance(Add(Sys.i, Sys.j), VectorAdd)

Out [3]: True

#### Other comments


#### Release Notes

<!-- BEGIN RELEASE NOTES -->

* vector
  * Added VectorKind
  * Add with vectors now calls VectorAdd

<!-- END RELEASE NOTES -->


Problem statement:
matrix of vectors destroys vector type
    from sympy.matrices import Matrix
    from sympy.vector import CoordSys3D, Vector

    Sys = CoordSys3D('Sys')
    O = Sys.origin

    # The row matrix of unit vectors
    E = Matrix([Sys.i, Sys.j, Sys.k]).T

    # A Column matrix of coordinates
    a = Matrix([1, 2, 3])

    # Obtain the matrix product
    av = E*a

    # The following does not work! Why is av[0] not a vector anymore? 
    # Is this a bug? Am I missing something important?
    A = O.locate_new('A', av[0])

Hints from issue discussion:
It is because:
```python
In [61]: type(Add(Sys.i, Sys.j))
Out[61]: sympy.core.add.Add

In [62]: type(Sys.i + Sys.j)
Out[62]: sympy.vector.vector.VectorAdd
```
The Matrix code assumes that it can add things with `Add` but there is no mechanism that makes this work with vectors.

Ideally the `.kind` would be used for this so that `Add` can dispatch to `VectorAdd` or something like that. Currently `.kind` is undefined for vectors though.
Do you think we can handle it any other way like checking the type and removing the vector part initially and adding them like scalars and adding them back later.


This sort of problem arises any time `Add(a, b)` is not the same as `a + b`. The vector code defines `__add__` and it is basically always a problem for an Expr class to define `__add__`. Instead what is needed is a way to make it so that `Add(a, b)` can do whatever should be done in the case of vectors.
> Ideally the .kind would be used for this so that Add can dispatch to VectorAdd or something like that. Currently .kind is undefined for vectors though.

What would you suggest on how to make these changes ,  I tried looking into it but it seems like it is connected to many different files and classes .

```
>>> E = Matrix([Sys.i, Sys.j, Sys.k]).T
>>> E[0].kind
UndefinedKind
>>> E.kind
MatrixKind(UndefinedKind)
```
is what we currently get . Do you think it should be changed so that `.kind` gives something like `VectorAdd` . 

> An expression representing a matrix may not be an instance of
     the Matrix class, but it will have kind ``MatrixKind``:

Sorry for any unnecessary questions but can't we make vectors work the same way ? So they can always have Something like `Vector` as its kind

Yes, ideally the kind of all vector objects would VectorKind.
Where would we need to make the changes to ensure the `.kind` gives the desired result for vectors .
Once one had implemented the VectorKind, what would be the next step to ensure that Add could dispatch to VectorAdd?
I'm not sure. Maybe look at how it works for MatrixKind.

Commit message: vector: updated VectorKind docstring

After making VectorKind a parametrised kind, we forgot
to update the docstring. It is updated now.

Patch to review:
diff --git a/sympy/vector/__init__.py b/sympy/vector/__init__.py
index 9228befbdb4b..f6757bbeb350 100644
--- a/sympy/vector/__init__.py
+++ b/sympy/vector/__init__.py
@@ -17,11 +17,14 @@
 from sympy.vector.implicitregion import ImplicitRegion
 from sympy.vector.parametricregion import (ParametricRegion, parametric_region_list)
 from sympy.vector.integrals import (ParametricIntegral, vector_integrate)
+from sympy.vector.kind import VectorKind
 
 __all__ = [
     'Vector', 'VectorAdd', 'VectorMul', 'BaseVector', 'VectorZero', 'Cross',
     'Dot', 'cross', 'dot',
 
+    'VectorKind',
+
     'Dyadic', 'DyadicAdd', 'DyadicMul', 'BaseDyadic', 'DyadicZero',
 
     'BaseScalar',
diff --git a/sympy/vector/kind.py b/sympy/vector/kind.py
new file mode 100644
index 000000000000..c6c04896b34c
--- /dev/null
+++ b/sympy/vector/kind.py
@@ -0,0 +1,67 @@
+#sympy.vector.kind
+
+from sympy.core.kind import Kind, _NumberKind, NumberKind
+from sympy.core.mul import Mul
+
+class VectorKind(Kind):
+    """
+    Kind for all vector objects in SymPy.
+
+    Parameters
+    ==========
+
+    element_kind : Kind
+        Kind of the element. Default is
+        :class:`sympy.core.kind.NumberKind`,
+        which means that the vector contains only numbers.
+
+    Examples
+    ========
+
+    Any instance of Vector class has kind ``VectorKind``:
+
+    >>> from sympy.vector.coordsysrect import CoordSys3D
+    >>> Sys = CoordSys3D('Sys')
+    >>> Sys.i.kind
+    VectorKind(NumberKind)
+
+    Operations between instances of Vector keep also have the kind ``VectorKind``:
+
+    >>> from sympy.core.add import Add
+    >>> v1 = Sys.i * 2 + Sys.j * 3 + Sys.k * 4
+    >>> v2 = Sys.i * Sys.x + Sys.j * Sys.y + Sys.k * Sys.z
+    >>> v1.kind
+    VectorKind(NumberKind)
+    >>> v2.kind
+    VectorKind(NumberKind)
+    >>> Add(v1, v2).kind
+    VectorKind(NumberKind)
+
+    Subclasses of Vector also have the kind ``VectorKind``, such as
+    Cross, VectorAdd, VectorMul or VectorZero.
+
+    See Also
+    ========
+
+    sympy.core.kind.Kind
+    sympy.matrices.kind.MatrixKind
+
+    """
+    def __new__(cls, element_kind=NumberKind):
+        obj = super().__new__(cls, element_kind)
+        obj.element_kind = element_kind
+        return obj
+
+    def __repr__(self):
+        return "VectorKind(%s)" % self.element_kind
+
+@Mul._kind_dispatcher.register(_NumberKind, VectorKind)
+def num_vec_mul(k1, k2):
+    """
+    The result of a multiplication between a number and a Vector should be of VectorKind.
+    The element kind is selected by recursive dispatching.
+    """
+    if not isinstance(k2, VectorKind):
+        k1, k2 = k2, k1
+    elemk = Mul._kind_dispatcher(k1, k2.element_kind)
+    return VectorKind(elemk)
diff --git a/sympy/vector/scalar.py b/sympy/vector/scalar.py
index 42742b021ea5..bcfb56cf177b 100644
--- a/sympy/vector/scalar.py
+++ b/sympy/vector/scalar.py
@@ -2,6 +2,7 @@
 from sympy.core.sympify import _sympify
 from sympy.printing.pretty.stringpict import prettyForm
 from sympy.printing.precedence import PRECEDENCE
+from sympy.core.kind import NumberKind
 
 
 class BaseScalar(AtomicExpr):
@@ -12,6 +13,8 @@ class BaseScalar(AtomicExpr):
 
     """
 
+    kind = NumberKind
+
     def __new__(cls, index, system, pretty_str=None, latex_str=None):
         from sympy.vector.coordsysrect import CoordSys3D
         if pretty_str is None:
diff --git a/sympy/vector/tests/test_vector.py b/sympy/vector/tests/test_vector.py
index b68fb9fb3efb..64cdc0cb8b59 100644
--- a/sympy/vector/tests/test_vector.py
+++ b/sympy/vector/tests/test_vector.py
@@ -1,4 +1,4 @@
-from sympy.core import Rational, S
+from sympy.core import Rational, S, Add, Mul
 from sympy.simplify import simplify, trigsimp
 from sympy.core.function import (Derivative, Function, diff)
 from sympy.core.numbers import pi
@@ -12,6 +12,8 @@
 from sympy.vector.coordsysrect import CoordSys3D
 from sympy.vector.vector import Cross, Dot, cross
 from sympy.testing.pytest import raises
+from sympy.vector.kind import VectorKind
+from sympy.core.kind import NumberKind
 
 C = CoordSys3D('C')
 
@@ -52,6 +54,50 @@ def test_vector_sympy():
     assert v3.__hash__() == v2.__hash__()
 
 
+def test_kind():
+    assert C.i.kind is VectorKind(NumberKind)
+    assert C.j.kind is VectorKind(NumberKind)
+    assert C.k.kind is VectorKind(NumberKind)
+
+    assert C.x.kind is NumberKind
+    assert C.y.kind is NumberKind
+    assert C.z.kind is NumberKind
+
+    assert Mul._kind_dispatcher(NumberKind, VectorKind(NumberKind)) is VectorKind(NumberKind)
+    assert Mul(2, C.i).kind is VectorKind(NumberKind)
+
+    v1 = C.x * i + C.z * C.z * j
+    v2 = C.x * i + C.y * j + C.z * k
+    assert v1.kind is VectorKind(NumberKind)
+    assert v2.kind is VectorKind(NumberKind)
+
+    assert (v1 + v2).kind is VectorKind(NumberKind)
+    assert Add(v1, v2).kind is VectorKind(NumberKind)
+    assert Cross(v1, v2).doit().kind is VectorKind(NumberKind)
+    assert VectorAdd(v1, v2).kind is VectorKind(NumberKind)
+    assert VectorMul(2, v1).kind is VectorKind(NumberKind)
+    assert VectorZero().kind is VectorKind(NumberKind)
+
+    assert v1.projection(v2).kind is VectorKind(NumberKind)
+    assert v2.projection(v1).kind is VectorKind(NumberKind)
+
+
+def test_vectoradd():
+    assert isinstance(Add(C.i, C.j), VectorAdd)
+    v1 = C.x * i + C.z * C.z * j
+    v2 = C.x * i + C.y * j + C.z * k
+    assert isinstance(Add(v1, v2), VectorAdd)
+
+    # https://github.com/sympy/sympy/issues/26121
+
+    E = Matrix([C.i, C.j, C.k]).T
+    a = Matrix([1, 2, 3])
+    av = E*a
+
+    assert av[0].kind == VectorKind()
+    assert isinstance(av[0], VectorAdd)
+
+
 def test_vector():
     assert isinstance(i, BaseVector)
     assert i != j
diff --git a/sympy/vector/vector.py b/sympy/vector/vector.py
index d64ea6d70890..4f5ff14600c6 100644
--- a/sympy/vector/vector.py
+++ b/sympy/vector/vector.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 from itertools import product
 
-from sympy.core.add import Add
+from sympy.core import Add, Basic
 from sympy.core.assumptions import StdFactKB
 from sympy.core.expr import AtomicExpr, Expr
 from sympy.core.power import Pow
@@ -14,6 +14,7 @@
     BasisDependent, BasisDependentMul, BasisDependentAdd)
 from sympy.vector.coordsysrect import CoordSys3D
 from sympy.vector.dyadic import Dyadic, BaseDyadic, DyadicAdd
+from sympy.vector.kind import VectorKind
 
 
 class Vector(BasisDependent):
@@ -34,6 +35,8 @@ class Vector(BasisDependent):
     _base_func: type[Vector]
     zero: VectorZero
 
+    kind: VectorKind = VectorKind()
+
     @property
     def components(self):
         """
@@ -344,6 +347,24 @@ def _div_helper(one, other):
         else:
             raise TypeError("Invalid division involving a vector")
 
+# The following is adapted from the matrices.expressions.matexpr file
+
+def get_postprocessor(cls):
+    def _postprocessor(expr):
+        vec_class = {Add: VectorAdd}[cls]
+        vectors = []
+        for term in expr.args:
+            if term.kind is VectorKind():
+                vectors.append(term)
+
+        if vec_class == VectorAdd:
+            return VectorAdd(*vectors).doit(deep=False)
+    return _postprocessor
+
+
+Basic._constructor_postprocessor_mapping[Vector] = {
+    "Add": [get_postprocessor(Add)],
+}
 
 class BaseVector(Vector, AtomicExpr):
     """


Code review comments:
diff_hunk: @@ -344,6 +347,24 @@ def _div_helper(one, other):
         else:
             raise TypeError("Invalid division involving a vector")
 
+# The following is adapted from the matrices.expressions.matexpr file
+
+def get_postprocessor(cls):
+    def _postprocessor(expr):
+        vec_class = {Add: VectorAdd}[cls]
+        vectors = []
+        for term in expr.args:
+            if term.kind is VectorKind():
path: sympy/vector/vector.py
text: @user1:
Should this not check something like `if isinstance(term.kind, VectorKind)` because the kind might not be `VectorKind(NumberKind)`?

@author:
Yes, it should. I'll change it now.