Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 49 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ 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](https://github.com/evtn/soda) and [evtn/soda-old](https://github.com/evtn/soda-old).
Both being graphics-oriented, so vector arithmetics is a must-have.
`Point` class builds up on my previous work with [evtn/soda](https://github.com/evtn/soda) and [evtn/soda-old](https://github.com/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).
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](https://gist.github.com/evtn/8683e58770f2901527275d46465e4cbe))

Both are refined and generalized for N dimensions. Some new additions (like `Point.transform(matrix: Matrix)`) are also in place.
Expand Down Expand Up @@ -53,7 +52,7 @@ p3 = Point.from_(1) # Point[1, ...]
# ...from another Point

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

Expand Down Expand Up @@ -130,7 +129,7 @@ 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).
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_`:

```python
Expand All @@ -147,9 +146,9 @@ Keep in mind that `Point.from_(int)` always produces a looped point, if you need

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
- `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

```python
a = Point(*range(5)) # Point[0, 1, 2, 3, 4]
Expand All @@ -158,14 +157,14 @@ a[2:4] # Point[2, 3, 4]
a[4, 3, 8, 2] # Point[4, 3, 0, 2]
```

Same applies for setting values on indices.
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).
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
Expand All @@ -179,6 +178,21 @@ b = a.named("B") # Point<B>[3, 4]

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

### Point to Dictionary

_New in 0.2.1_

You can convert any Point into a dictionary with defined keys using `Point.as_` or `Point.to_dict`

```python
from easypoint import Point

p = Point(1, 2, 3)

p.to_dict("x", "y", "z") # { "x": 1, "y": 2, "z": 3 }
p.to_dict("x", "y") # { "x": 1, "y": 2 }
p.to_dict("x", None, "z") # { "x": 1, "z": 3 }
```

### FnPoint

Expand All @@ -201,9 +215,9 @@ fp[4] # 0

```

It is fully compatible with Point, but any operation on FnPoint will return you a new, derived FnPoint.
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)`
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:

```python
Expand All @@ -217,15 +231,15 @@ fp_fin.concrete()
fp_slice.concrete()

# error
fp.concrete()
fp.concrete()

```

## Matrix

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

Matrices are N-dimensional tables, well, you can [read Wikipedia](https://en.wikipedia.org/wiki/Matrix_(mathematics)) instead of this.
Matrices are N-dimensional tables, well, you can [read Wikipedia](<https://en.wikipedia.org/wiki/Matrix_(mathematics)>) instead of this.

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

Expand Down Expand Up @@ -262,8 +276,8 @@ for index in mul_table.iter((3, 3), (4, 5)):

### 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).
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`:

```python
Expand Down Expand Up @@ -306,16 +320,16 @@ for (y, x) in coord_table:

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.
- `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.
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.

```python
Expand All @@ -329,10 +343,9 @@ matrix[32474, 2387] # 327
matrix.data # {3247399969913: 327}
```

If you need to swap the storage for something more efficient, build your own class.
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:


```python
from easypoint import Matrix
from easypoint.internal_types import Size, Index, MatrixIndexFunc
Expand All @@ -342,31 +355,30 @@ 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
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
- Better docs?
- Proper conversion from list to Matrix (although it's fairly easy now)
- Better test coverage
4 changes: 2 additions & 2 deletions easypoint/internal_types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations
from typing import Callable, List, Optional, Tuple, Union


PointLike = Union["Point", float, List[float], Tuple[float, ...]]
BasePointLike = Union[float, List[float], Tuple[float, ...]]
PointLike = Union["Point", BasePointLike]

Index = Tuple[int, ...]
Size = Index
Expand Down
69 changes: 65 additions & 4 deletions easypoint/point.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,53 @@

from math import cos, hypot, sin, radians as deg_to_rad
from typing import (
Any,
Callable,
Iterator,
Sequence,
TypeVar,
cast,
overload,
)
from typing_extensions import Self


def applier(func: Merger) -> ApplyFunc:
return lambda s, o: Point.from_(s).apply(Point.from_(o), func)
@overload
def apply(s: FnPoint, o: PointLike, func: Merger) -> FnPoint:
...


@overload
def apply(s: PointLike, o: FnPoint, func: Merger) -> FnPoint:
...


@overload
def apply(s: _P, o: PointLike, func: Merger) -> _P:
...


@overload
def apply(s: BasePointLike, o: _P, func: Merger) -> _P:
...


@overload
def apply(s: PointLike, o: PointLike, func: Merger) -> Point:
...


def apply(s: PointLike, o: PointLike, func: Merger) -> Point:
return Point.from_(s).apply(Point.from_(o), func)


def applier(func: Merger):
def apply(s: PointLike, o: PointLike):
return Point.from_(s).apply(Point.from_(o), func)

return apply


_P = TypeVar("_P", bound="Point")


class Point(Sequence[float]):
Expand All @@ -28,14 +63,37 @@ def named(self, new_name: str) -> Point:
new.name = new_name
return new

@overload
@staticmethod
def from_(value: _P) -> _P:
...

@overload
@staticmethod
def from_(value: PointLike) -> Point:
...

@staticmethod
def from_(value: PointLike, loop: bool = False) -> Point:
def from_(value: _P | PointLike, loop: bool = False) -> _P | Point:
if isinstance(value, Point):
return value
if isinstance(value, (list, tuple)):
return Point(*value, loop=loop)
return Point(value, loop=True)

def as_(self, *argnames: str | None) -> dict[str, float]:
result: dict[str, float] = {}

for i, argname in enumerate(argnames):
if not argname:
continue

result[argname] = self[i]

return result

to_dict = as_

@overload
def __getitem__(self, item: int) -> float:
...
Expand Down Expand Up @@ -174,6 +232,9 @@ def transform(self, matrix: Matrix) -> Point:
"""
Transforms a vecN using a given NxN matrix.
"""
if len(matrix.size) != 2:
raise ValueError("Cannot transform a vector using a non-2d matrix")

self_matrix = Matrix.as_matrix(self).transpose()

product = matrix * self_matrix
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "easypoint"
version = "0.2.0"
version = "0.2.1"
description = "Minimal general-purpose vector / matrix arithmetics library"
authors = ["Dmitry Gritsenko <k01419q45@ya.ru>"]
license = "MIT"
Expand Down
Empty file added test/__init__.py
Empty file.