Package: easypoint
Minimal general-purpose vector / matrix arithmetics libraryInstallationInstalleasypointmodule:poetryaddeasypointorpython-mpipinstalleasypointIntroductioneasypoint has 2 main types to work with:Point(a.k.a.Vector) andMatrixPointclass builds up on my previous work withevtn/sodaandevtn/soda-old.Both being graphics-oriented, so vector arithmetics is a must-have.But over time,Pointbecame a convenient class for various non-graphical tasks and tasks out of scope forsoda(e.g. raster graphics).This module also brings a refinedMatrixclass I've been using in various private/unfinished projects (an old version can be seenhere)Both are refined and generalized for N dimensions. Some new additions (likePoint.transform(matrix: Matrix)) are also in place.UsagePointPoint/Vector (easypoint.Vectoris just an alias) is a fancySequence[float], supporting various convenient operations.Make a pointfromeasypointimportPoint# create a Point with numbers:p1=Point(1,2,3)# Point[1, 2, 3]# ...or from a list/tuplep2=Point.from_([1,2,3])# Point[1, 2, 3]# ...from a numberp3=Point.from_(1)# Point[1, ...]# ...from another Pointp4=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)MathYou 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]DistanceYou also can calculate distance between points and get a normalized vector:frommathimportpia.distance(b)# 4.242640687119285a.distance()# 2.23606797749979 (distance between a and (0, 0), basically the length of a vector)a.normalized()# Point[0.4472135954999579, 0.8944271909999159]Rotation2D 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]TransformsYou can transform an N-dimensionalPointwith a NxNMatrix:fromeasypointimportPoint,Matrix# Shearing# 1 k# 0 1matrix=Matrix.as_matrix((1,4),(0,1))t=Point(0,5)t.transform(matrix)# Point[20, 5]Looped pointsSometimes it's convenient to have a point withp[i] == p[i % n](a repeating set of coordinates).It can be achieved by passingloop=TrueintoPointconstructor orPoint.from_:p1=Point(10.3,loop=True)# Point[10.3, ...]p1[54378]# 10.3p2=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 thatPoint.from_(int)always produces a looped point, if you need a 1-dimensional point, usePoint(int)IndexingPoints 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 aPointwith values under that slicepoint[tuple[int, ...]]returns a Point with values under indices in the tuplea=Point(*range(5))# Point[0, 1, 2, 3, 4]a[2]# 2a[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 ignoredThere are alsox,y, andzproperties as aliases for[0],[1], and[2]InterpolationFor convenience, there arepoint.interpolate(other: PointLike, k: float)to interpolate between two points (self at 0, other at 1).point.center(other: PointLike)is an alias forpoint.interpolate(other, 0.5)NamingYou 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 renamedFnPointFnPoint is a Point defined by index function:fromeasypointimportFnPointfp=FnPoint(lambdai:126*i*i+7)fp[4]# 2023...and optional length:fromeasypointimportFnPointfp=FnPoint(lambdai:126*i*i+7,length=3)fp[4]# 0It 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 concretePointinstance, callfp.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(lambdax:x*2)# infinite pointfp_fin=FnPoint(lambdax:x*2,length=4)# finite pointfp_slice=fp[:4]# also finite# okayfp_fin.concrete()fp_slice.concrete()# errorfp.concrete()MatrixNow you can wake up and take a non-pointy pill, at last.Matrices are N-dimensional tables, well, you canread Wikipediainstead of this.Ineasypoint, matrices are quite straightforward (keep in mind, they have 0-based indexing):fromeasypointimportMatrixmul_table=Matrix((10,10))for(y,x)inmul_table:mul_table[y,x]=(y+1)*xfrompprintimportpprint"""[[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, usematrix.iter()explicitly:# Matrix.iter(self, start: Index | None = None, stop: Index | None = None):mul_table=Matrix((10,10))forindexinmul_table.iter((3,3),(4,5)):mul_table[index]=0# why? idkOperationsAs withPoint, 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 useMatrix.apply_bin:fromeasypointimportMatrixx_table=Matrix((10,10))y_table=x_table.new()# creates a new matrix of the same sizefor(y,x)inx_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 debugmul_table=x_table.apply_bin(y_table,lambdax,y:x*y,op="*")frompprintimportpprint"""[[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 withapply:coord_table=Matrix((10,10))for(y,x)incoord_table:coord_table[y,x]=(x+y)for(y,x)incoord_table:coord_table.apply(lambdax:-x)# same as coord_table * -1Other methods defined:matrix.new()creates an empty matrix of the same size (same asMatrix(matrix.size)),matrix.copy()copies the matrix (same asmatrix.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 indexi. For example, used on a 2D matrix, returns ani-th row.matrix.as_matrix(*points: PointLike)builds a 2D matrix out of Point-like values.Internal stateMatrices ineasypointare 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]# 0matrix.data# {}matrix[32474,2387]=327matrix[32474,2387]# 327matrix.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-onlyFnMatrixclass:fromeasypointimportMatrixfromeasypoint.internal_typesimportSize,Index,MatrixIndexFuncclassFnMatrix(Matrix):def__init__(self,size:Size,fn:MatrixIndexFunc):self.fn=fnself.size=sizedefget_index(self,index:Index):returnself.fn(index)defset_index(self,index:Index,value:float):raiseValueError("this matrix is read-only")defcopy(self):returnFnMatrix(self.size,self.fn)defnew(self):returnself.copy()defsum_func(index:Index):x,y=indexreturnx+yfnm=FnMatrix(sum_func)TODOBetter docs?Proper conversion from list to Matrix (although it's fairly easy now)Better test coverage