Skip to content

Commit

Permalink
Add matrix multiplier methods
Browse files Browse the repository at this point in the history
  • Loading branch information
mwtoews committed Jan 26, 2025
1 parent 756471b commit 808bbcb
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CHANGES
(#111).
- Source was moved to a single-module affine.py in the src directory (#112).
- Add numpy __array__ interface (#108).
- Add support for ``@`` matrix multiplier methods (#122).

2.4.0 (2023-01-19)
------------------
Expand Down
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ Matrices can be created by passing the values ``a, b, c, d, e, f`` to the
Affine(0.7071067811865476, -0.7071067811865475, 0.0,
0.7071067811865475, 0.7071067811865476, 0.0)
These matrices can be applied to ``(x, y)`` tuples to obtain transformed
coordinates ``(x', y')``.
These matrices can be applied to ``(x, y)`` tuples using the
``*`` operator (or the ``@`` matrix multiplier operator for
future releases) to obtain transformed coordinates ``(x', y')``.

.. code-block:: pycon
Expand Down Expand Up @@ -91,7 +92,7 @@ origin can be easily computed.
>>> fwd * (col, row)
(-237481.5, 195036.4)
The reverse transformation is obtained using the ``~`` operator.
The reverse transformation is obtained using the ``~`` inverse operator.

.. code-block:: pycon
Expand Down
74 changes: 66 additions & 8 deletions src/affine.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from collections.abc import MutableSequence, Sequence
from functools import cached_property
import math
import warnings

from attrs import astuple, define, field

Expand Down Expand Up @@ -549,20 +550,20 @@ def __add__(self, other):

__iadd__ = __add__

def __mul__(self, other):
"""Multiplication.
def __matmul__(self, other):
"""Matrix multiplication.
Apply the transform using matrix multiplication, creating
a resulting object of the same type. A transform may be applied
to another transform, a vector, vector array, or shape.
to another transform or vector array.
Parameters
----------
other : Affine or iterable of (vx, vy)
other : Affine or iterable of (vx, vy, [vw])
Returns
-------
Affine or a tuple of two floats
Affine or a tuple of two or three floats
"""
sa, sb, sc, sd, se, sf = self[:6]
if isinstance(other, Affine):
Expand All @@ -575,16 +576,73 @@ def __mul__(self, other):
sd * ob + se * oe,
sd * oc + se * of + sf,
)
# vector of 2 or 3 values
try:
other = tuple(map(float, other))
except (TypeError, ValueError):
return NotImplemented
num_values = len(other)
if num_values == 2:
vx, vy = other
return (vx * sa + vy * sb + sc, vx * sd + vy * se + sf)
elif num_values == 3:
vx, vy, vw = other
if vw != 1.0:
raise ValueError("third value must be 1.0")
else:
raise TypeError("expected vector of 2 or 3 values")
px = vx * sa + vy * sb + sc
py = vx * sd + vy * se + sf
if num_values == 2:
return (px, py)
return (px, py, vw)

def __rmatmul__(self, other):
return NotImplemented

def __imatmul__(self, other):
if not isinstance(other, Affine):
raise TypeError("Operation not supported")
return NotImplemented

def __mul__(self, other):
"""Multiplication.
Apply the transform using matrix multiplication, creating
a resulting object of the same type. A transform may be applied
to another transform, a vector, vector array, or shape.
Parameters
----------
other : Affine or iterable of (vx, vy)
Returns
-------
Affine or a tuple of two floats
"""
# TODO: consider enabling this for 3.1
# warnings.warn(
# "Use `@` matmul instead of `*` mul operator for matrix multiplication",
# PendingDeprecationWarning,
# stacklevel=2,
# )
if isinstance(other, Affine):
return self.__matmul__(other)
try:
_, _ = other
return self.__matmul__(other)
except (ValueError, TypeError):
return NotImplemented

def __rmul__(self, other):
return NotImplemented

def __imul__(self, other):
if isinstance(other, tuple):
warnings.warn(
"in-place multiplication with tuple is deprecated",
DeprecationWarning,
stacklevel=2,
)
return NotImplemented

def itransform(self, seq: MutableSequence[Sequence[float]]) -> None:
Expand Down Expand Up @@ -667,7 +725,7 @@ def loadsw(s: str) -> Affine:
raise ValueError(f"Expected 6 coefficients, found {len(coeffs)}")
a, d, b, e, c, f = (float(x) for x in coeffs)
center = Affine(a, b, c, d, e, f)
return center * Affine.translation(-0.5, -0.5)
return center @ Affine.translation(-0.5, -0.5)


def dumpsw(obj: Affine) -> str:
Expand All @@ -680,7 +738,7 @@ def dumpsw(obj: Affine) -> str:
-------
str
"""
center = obj * Affine.translation(0.5, 0.5)
center = obj @ Affine.translation(0.5, 0.5)
return "\n".join(repr(getattr(center, x)) for x in list("adbecf")) + "\n"


Expand Down
29 changes: 28 additions & 1 deletion tests/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from affine import Affine
from affine import Affine, identity

try:
import numpy as np
Expand Down Expand Up @@ -66,3 +66,30 @@ def test_linalg():
)
testing.assert_allclose(~tfm, expected_inv)
testing.assert_allclose(np.linalg.inv(ar), expected_inv)


def test_matmul():
A = Affine(2, 0, 3, 0, 3, 2)
Ar = np.array(A)

# matrix @ matrix = matrix
res = A @ identity
assert isinstance(res, Affine)
testing.assert_equal(res, Ar)
res = Ar @ np.eye(3)
assert isinstance(res, np.ndarray)
testing.assert_equal(res, Ar)

# matrix @ vector = vector
v = (2, 3, 1)
vr = np.array(v)
expected_p = (7, 11, 1)
res = A @ v
assert isinstance(res, tuple)
testing.assert_equal(res, expected_p)
res = A @ vr
assert isinstance(res, tuple)
testing.assert_equal(res, expected_p)
res = Ar @ vr
assert isinstance(res, np.ndarray)
testing.assert_equal(res, expected_p)
8 changes: 4 additions & 4 deletions tests/test_rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ def test_rotation_angle():
|
0---------*
Affine.rotation(45.0) * (1.0, 0.0) == (0.707..., 0.707...)
Affine.rotation(45.0) @ (1.0, 0.0) == (0.707..., 0.707...)
|
| *
|
|
0----------
"""
x, y = Affine.rotation(45.0) * (1.0, 0.0)
x, y = Affine.rotation(45.0) @ (1.0, 0.0)
sqrt2div2 = math.sqrt(2.0) / 2.0
assert x == pytest.approx(sqrt2div2)
assert y == pytest.approx(sqrt2div2)
Expand Down Expand Up @@ -55,8 +55,8 @@ def test_rotation_matrix_pivot():
rot = Affine.rotation(90.0, pivot=(1.0, 1.0))
exp = (
Affine.translation(1.0, 1.0)
* Affine.rotation(90.0)
* Affine.translation(-1.0, -1.0)
@ Affine.rotation(90.0)
@ Affine.translation(-1.0, -1.0)
)
for r, e in zip(rot, exp):
assert r == pytest.approx(e)
Loading

0 comments on commit 808bbcb

Please sign in to comment.