Skip to content
/ easypoint Public

Minimal general-purpose vector / matrix arithmetics library

License

Notifications You must be signed in to change notification settings

evtn/easypoint

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Minimal general-purpose vector / matrix arithmetics library

Installation

Install easypoint module:

poetry add easypoint

or

python -m pip install easypoint

Introduction

easypoint has 2 main types to work with: Point (a.k.a. Vector) and Matrix

Point class builds up on my previous work with evtn/soda and evtn/soda-old.
Both being graphics-oriented, so vector arithmetics is a must-have.

But over time, Point became a convenient class for various non-graphical tasks and tasks out of scope for soda (e.g. raster graphics).
This module also brings a refined Matrix class I've been using in various private/unfinished projects (an old version can be seen here)

Both are refined and generalized for N dimensions. Some new additions (like Point.transform(matrix: Matrix)) are also in place.

Usage

Point

Point/Vector (easypoint.Vector is just an alias) is a fancy Sequence[float], supporting various convenient operations.

Make a point

from easypoint import Point

# create a Point with numbers:

p1 = Point(1, 2, 3) # Point[1, 2, 3]

# ...or from a list/tuple

p2 = Point.from_([1, 2, 3]) # Point[1, 2, 3]

# ...from a number

p3 = Point.from_(1) # Point[1, ...]

# ...from another Point

p4 = p1[:2] # Point[1, 2]
p5 = p1[0, 2, 1] # Point[1, 3, 2] 
p6 = p3[:] # Error (a slice of an infinite Point)
a = Point(1, 2)
b = Point(4, 5)

In any context where a point could be used, "point-like" values can be used:

  • (1, 2) <-> Point(1, 2)
  • [1, 2] <-> Point(1, 2)
  • 1 <-> Point(1, loop=True)

Math

You can perform mathematical operations on points (element-wise):

a + b # Point[5, 7]
a - b # Point[-3, -3]
a * b # Point[4, 10]
a / b # Point[0.25, 0.4]
a % b # Point[1, 2]

...and any point-like values:

a + 10 # Point[11, 12]
a * 2 # Point[2, 4]

Distance

You also can calculate distance between points and get a normalized vector:

from math import pi

a.distance(b) # 4.242640687119285
a.distance() # 2.23606797749979 (distance between a and (0, 0), basically the length of a vector)
a.normalized() # Point[0.4472135954999579, 0.8944271909999159]

Rotation

2D Rotation can be done around some center:

a.rotate2d(degrees=90) # Point[-2, 1]
a.rotate2d(center=(10, 10), radians=pi / 2) # Point[18, 1]
a.rotate2d(center=10, degrees=90) # Point[18, 1]

# if you want to use axis other than (0, 1), pass `axis`:
c = Point(4, 6, 2, 3, 2)
c.rotate2d(center=10, radians=pi / 2, axis=(3, 4)) # Point[4, 6, 2, 18, 3]

Transforms

You can transform an N-dimensional Point with a NxN Matrix:

from easypoint import Point, Matrix
# Shearing
# 1 k
# 0 1

matrix = Matrix.as_matrix((1, 4), (0, 1))
t = Point(0, 5)
t.transform(matrix) # Point[20, 5]

Looped points

Sometimes it's convenient to have a point with p[i] == p[i % n] (a repeating set of coordinates).
It can be achieved by passing loop=True into Point constructor or Point.from_:

p1 = Point(10.3, loop=True) # Point[10.3, ...]
p1[54378] # 10.3

p2 = Point.from_([1, 2, 3], loop=True) # Point[1, 2, 3, ...]
p2[540:550] # Point[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

Keep in mind that Point.from_(int) always produces a looped point, if you need a 1-dimensional point, use Point(int)

Indexing

Points support three types of indexing:

  • point[int] returns a value at that index, or 0 if this index doesn't exist (and the point is not looped)
  • point[slice] returns a Point with values under that slice
  • point[tuple[int, ...]] returns a Point with values under indices in the tuple
a = Point(*range(5)) # Point[0, 1, 2, 3, 4]
a[2] # 2
a[2:4] # Point[2, 3, 4]
a[4, 3, 8, 2] # Point[4, 3, 0, 2]

Same applies for setting values on indices.
Keep in mind that setting a slice/tuple doesn't change the dimension count, extra indices/values are ignored

There are also x, y, and z properties as aliases for [0], [1], and [2]

Interpolation

For convenience, there are point.interpolate(other: PointLike, k: float) to interpolate between two points (self at 0, other at 1).
point.center(other: PointLike) is an alias for point.interpolate(other, 0.5)

Naming

You can give any point a name (any string) for convenience and better output:

a = Point(3, 4) # Point[3, 4]
b = a.named("B") # Point<B>[3, 4]

Naming returns a copy of the point, so the original one is not renamed

FnPoint

FnPoint is a Point defined by index function:

from easypoint import FnPoint

fp = FnPoint(lambda i: 126 * i * i + 7)
fp[4] # 2023

...and optional length:

from easypoint import FnPoint

fp = FnPoint(lambda i: 126 * i * i + 7, length=3)
fp[4] # 0

It is fully compatible with Point, but any operation on FnPoint will return you a new, derived FnPoint.

If you want (for some reason) to get a concrete Point instance, call fp.concrete(loop: bool = False)
Obviously, this will raise an error on an infinite FnPoint, so either pass a length into the constructor or as a slice:

fp = FnPoint(lambda x: x * 2) # infinite point
fp_fin = FnPoint(lambda x: x * 2, length=4) # finite point
fp_slice = fp[:4] # also finite

# okay
fp_fin.concrete()
fp_slice.concrete()

# error
fp.concrete() 

Matrix

Now you can wake up and take a non-pointy pill, at last.

Matrices are N-dimensional tables, well, you can read Wikipedia instead of this.

In easypoint, matrices are quite straightforward (keep in mind, they have 0-based indexing):

from easypoint import Matrix


mul_table = Matrix((10, 10))

for (y, x) in mul_table:
    mul_table[y, x] = (y + 1) * x

from pprint import pprint

"""
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 ...,
 [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
"""
pprint(mul_table.to_list())

As you can see, one can easily iterate over every index in matrix using iterator protocol. If you want to iterate over some portion of a matrix, use matrix.iter() explicitly:

# Matrix.iter(self, start: Index | None = None, stop: Index | None = None):

mul_table = Matrix((10, 10))

for index in mul_table.iter((3, 3), (4, 5)):
    mul_table[index] = 0 # why? idk

Operations

As with Point, with matrices you can get an element-wise sum, difference and multiply matrix by a number.
Multiplication (as well as @) is reserved for matrix multiplication (or, generally, tensor contraction).
If you need an element-wise multiplication (or any other operation), you can use Matrix.apply_bin:

from easypoint import Matrix

x_table = Matrix((10, 10))
y_table = x_table.new() # creates a new matrix of the same size

for (y, x) in x_table:
    x_table[y, x] = (x + 1)
    y_table[y, x] = (y + 1)

# Matrix.apply_bin(self, other: Matrix, func: Callable[[float, float], float], op: str = "?")
# `op` param is optional, it is an arbitrary string used for better debug
mul_table = x_table.apply_bin(y_table, lambda x, y: x * y, op="*")

from pprint import pprint

"""
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 ...,
 [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
"""
pprint(mul_table.to_list())

You can also apply a function to a single matrix with apply:

coord_table = Matrix((10, 10))

for (y, x) in coord_table:
    coord_table[y, x] = (x + y)

for (y, x) in coord_table:
    coord_table.apply(lambda x: -x) # same as coord_table * -1

Other methods defined:

  • matrix.new() creates an empty matrix of the same size (same as Matrix(matrix.size)),
  • matrix.copy() copies the matrix (same as matrix.apply(x: x))
  • matrix.transpose() transposes the matrix (wow!)
  • matrix.cut(index) returns a new matrix where all the rows/columns/etc. that pass through a specific index are removed.
  • matrix.get_submatrix(i: int) for an N-dimensional matrix, returns an (N-1)-dimensional matrix at some index i. For example, used on a 2D matrix, returns an i-th row.
  • matrix.as_matrix(*points: PointLike) builds a 2D matrix out of Point-like values.

Internal state

Matrices in easypoint are implemented as flat dictionaries, with empty (default) values are omitted.
This helps with memory and speed if you have sparse matrices.

matrix = Matrix((99999999, 99999999))

matrix[32474, 2387] # 0
matrix.data # {}

matrix[32474, 2387] = 327
matrix[32474, 2387] # 327
matrix.data # {3247399969913: 327}

If you need to swap the storage for something more efficient, build your own class.
For example, here's an example of possible read-only FnMatrix class:

from easypoint import Matrix
from easypoint.internal_types import Size, Index, MatrixIndexFunc


class FnMatrix(Matrix):
    def __init__(self, size: Size, fn: MatrixIndexFunc):
        self.fn = fn
        self.size = size
    
    def get_index(self, index: Index):
        return self.fn(index)
    
    def set_index(self, index: Index, value: float):
        raise ValueError("this matrix is read-only")
    
    def copy(self):
        return FnMatrix(self.size, self.fn)
    
    def new(self):
        return self.copy()
    

def sum_func(index: Index):
    x, y = index
    return x + y    


fnm = FnMatrix(sum_func)

TODO

  • Better docs?
  • Proper conversion from list to Matrix (although it's fairly easy now)
  • Better test coverage

About

Minimal general-purpose vector / matrix arithmetics library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages