Repository: mpmath/mpmath
Pull request #897 (2025-01-06 02:05:44)

Misc fixes
* drop "4-tuple" note in docs, per #720
* ignore .hypothesis/ in pytest
* Support negative indexes in matrix, closes 835


Patch:
diff --git a/docs/technical.rst b/docs/technical.rst
index fb8db87..135e08b 100644
--- a/docs/technical.rst
+++ b/docs/technical.rst
@@ -63,8 +63,6 @@ The representation as defined so far is not unique; one can always multiply the
 
 Simple mathematical operations are now easy to define. Due to uniqueness, equality testing of two numbers simply amounts to separately checking equality of the mantissas and the exponents. Multiplication of nonzero numbers is straightforward: `(m 2^e) \times (n 2^f) = (m n) \times 2^{e+f}`. Addition is a bit more involved: we first need to multiply the mantissa of one of the operands by a suitable power of 2 to obtain equal exponents.
 
-More technically, mpmath represents a floating-point number as a 4-tuple *(sign, man, exp, bc)* where *sign* is 0 or 1 (indicating positive vs negative) and the mantissa is nonnegative; *bc* (*bitcount*) is the size of the absolute value of the mantissa as measured in bits. Though redundant, keeping a separate sign field and explicitly keeping track of the bitcount significantly speeds up arithmetic (the bitcount, especially, is frequently needed but slow to compute from scratch due to the lack of a Python built-in function for the purpose).
-
 Contrary to popular belief, floating-point *numbers* do not come with an inherent "small uncertainty", although floating-point *arithmetic* generally is inexact. Every binary floating-point number is an exact rational number. With arbitrary-precision integers used for the mantissa and exponent, floating-point numbers can be added, subtracted and multiplied *exactly*. In particular, integers and integer multiples of 1/2, 1/4, 1/8, 1/16, etc. can be represented, added and multiplied exactly in binary floating-point arithmetic.
 
 Floating-point arithmetic is generally approximate because the size of the mantissa must be limited for efficiency reasons. The maximum allowed width (bitcount) of the mantissa is called the precision or *prec* for short. Sums and products of floating-point numbers are exact as long as the absolute value of the mantissa is smaller than `2^{prec}`. As soon as the mantissa becomes larger than this, it is truncated to contain at most *prec* bits (the exponent is incremented accordingly to preserve the magnitude of the number), and this operation introduces a rounding error. Division is also generally inexact; although we can add and multiply exactly by setting the precision high enough, no precision is high enough to represent for example 1/3 exactly (the same obviously applies for roots, trigonometric functions, etc).
diff --git a/mpmath/matrices/matrices.py b/mpmath/matrices/matrices.py
index e0ece4d..84d8459 100644
--- a/mpmath/matrices/matrices.py
+++ b/mpmath/matrices/matrices.py
@@ -108,6 +108,14 @@ class _matrix:
         >>> x[1,0]
         mpf('2.0')
 
+    It is also possible to access matrices and vectors via negative index::
+        >>> x = matrix([[1, 2], [3, 4]])
+        >>> y = matrix([6, 7])
+        >>> x[-1, -2]
+        mpf('3.0')
+        >>> y[-2]
+        mpf('6.0')
+
     Other
     .....
 
@@ -469,6 +477,9 @@ def __getitem__(self, key):
                     raise IndexError('Row index out of bounds')
                 rows = [key[0]]
 
+                if(key[0] < 0 and key[0] >= - self._rows):
+                    rows[0] += self._rows
+
             # Columns
             if isinstance(key[1],slice):
                 # Check bounds
@@ -485,6 +496,9 @@ def __getitem__(self, key):
                     raise IndexError('Column index out of bounds')
                 columns = [key[1]]
 
+                if(key[1] < 0 and key[1] >= - self._cols):
+                    columns[0] += self._cols
+
             # Create matrix slice
             m = self.ctx.matrix(len(rows),len(columns))
 
@@ -496,6 +510,22 @@ def __getitem__(self, key):
             return m
 
         else:
+            row, col = key
+
+            if row < 0:
+                if -row <= self._rows:
+                    row = self._rows + row
+                else:
+                    raise IndexError('matrix index out of range')
+
+            if col < 0:
+                if -col <= self._cols:
+                    col = self._cols + col
+                else:
+                    raise IndexError('matrix index out of range')
+
+            key = (row, col)
+
             # single element extraction
             if key[0] >= self._rows or key[1] >= self._cols:
                 raise IndexError('matrix index out of range')
@@ -534,6 +564,9 @@ def __setitem__(self, key, value):
             else:
                 # Single row
                 rows = [key[0]]
+
+                if(key[0] < 0 and key[0] >= - self._rows):
+                    rows[0] += self._rows
             # Columns
             if isinstance(key[1],slice):
                 # Check bounds
@@ -546,6 +579,9 @@ def __setitem__(self, key, value):
             else:
                 # Single column
                 columns = [key[1]]
+
+                if(key[1] < 0 and key[1] >= - self._cols):
+                    columns[0] += self._cols
             # Assign slice with a scalar
             if isinstance(value,self.ctx.matrix):
                 # Assign elements to matrix if input and output dimensions match
@@ -564,6 +600,22 @@ def __setitem__(self, key, value):
         else:
             # Single element assingment
             # Check bounds
+            row, col = key
+
+            if row < 0:
+                if -row <= self._rows:
+                    row = self._rows + row
+                else:
+                    raise IndexError('matrix index out of range')
+
+            if col < 0:
+                if -col <= self._cols:
+                    col = self._cols + col
+                else:
+                    raise IndexError('matrix index out of range')
+
+            key = (row, col)
+
             if key[0] >= self._rows or key[1] >= self._cols:
                 raise IndexError('matrix index out of range')
             # Convert and store value
diff --git a/pyproject.toml b/pyproject.toml
index 90f6ebb..e0574b1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -58,7 +58,7 @@ max_line_length = 200
 testpaths = ['mpmath', 'docs']
 doctest_optionflags = ['IGNORE_EXCEPTION_DETAIL', 'ELLIPSIS']
 addopts = "--durations=20 --doctest-modules --doctest-glob='*.rst'"
-norecursedirs = ['docs/plots', 'demo', '.eggs', '.git']
+norecursedirs = ['docs/plots', 'demo', '.eggs', '.git', '.hypothesis']
 filterwarnings = ['error::DeprecationWarning']
 xfail_strict = true
 timeout = 600


Test patch:
diff --git a/mpmath/tests/test_matrices.py b/mpmath/tests/test_matrices.py
index f08707f..2e56d18 100644
--- a/mpmath/tests/test_matrices.py
+++ b/mpmath/tests/test_matrices.py
@@ -57,6 +57,24 @@ def test_matrix_basic():
     pytest.raises(IndexError, lambda: zeros(1,1)[:, 1])  # issue 318
     pytest.raises(IndexError, lambda: zeros(1,1)[1, :])
 
+    A10 = matrix([[1,2], [3,4], [5,6]])
+    assert A10[0, -1] == 2
+    assert A10[-1, -1] == 6
+    assert A10[1, -2] == 3
+    assert A10[-3, -2] == 1
+
+    A10[0, -1] = 3
+    assert A10[0, -1] == 3
+
+    A10[-1, -1] = 4
+    assert A10[-1, -1] == 4
+
+    A10[1, -2] = 5
+    assert A10[1, -2] == 5
+
+    A10[-3, -2] = 1
+    assert A10[-3, -2] == 1
+
 def test_matmul():
     """
     Test the PEP465 "@" matrix multiplication syntax.
@@ -79,6 +97,11 @@ def test_matrix_slices():
     assert A[0:2,0:2] == matrix([[1,2],[4,5]])  # issue 267
     assert A[:2,:2] == matrix([[1,2],[4,5]])
     assert V[2:4] == matrix([3,4])
+    assert A[-1, :] == A[2, :]
+    assert A[:, -1] == A[:, 2]
+
+    pytest.raises(IndexError, lambda: A[-4, 0])
+    pytest.raises(IndexError, lambda: A[0, -4])
     pytest.raises(IndexError, lambda: A[:,1:6])
 
     # Assign slice with matrix
@@ -116,6 +139,27 @@ def test_matrix_slices():
     for x in A1:
         assert x == 40
 
+    # test negative indexes
+    A2 = matrix(3)
+    A2[:,:] = A
+
+    A2[-3, :] = matrix([[10, 11, 12]])
+    assert A2 == matrix([[10, 11, 12],
+                         [4, 5, 6],
+                         [7, 8, 9]])
+    A2[:,-1] = matrix([[13], [14], [15]])
+    assert A2 == matrix([[10, 11, 13],
+                         [4, 5, 14],
+                         [7, 8, 15]])
+    A2[:-1,:-1] = matrix([[16, 17], [18, 19]])
+    assert A2 == matrix([[16, 17, 13],
+                         [18, 19, 14],
+                         [7, 8 ,15]])
+
+    with pytest.raises(IndexError):
+        A2[-123,1] = 123
+    with pytest.raises(IndexError):
+        A2[1,-123] = 123
 
 def test_matrix_power():
     A = matrix([[1, 2], [3, 4]])