Skip to content

Commit

Permalink
Merge pull request #26 from InCogNiTo124/refactor-to-files
Browse files Browse the repository at this point in the history
Refactor all methods to their respective files
  • Loading branch information
InCogNiTo124 authored Nov 1, 2021
2 parents 2435b7f + dda2e48 commit 088e54b
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 181 deletions.
24 changes: 24 additions & 0 deletions docs/api/apidoc/knarrow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ knarrow package
Submodules
----------

knarrow.angle\_method module
----------------------------

.. automodule:: knarrow.angle_method
:members:
:undoc-members:
:show-inheritance:

knarrow.c\_method module
------------------------

Expand All @@ -12,6 +20,14 @@ knarrow.c\_method module
:undoc-members:
:show-inheritance:

knarrow.distance\_method module
-------------------------------

.. automodule:: knarrow.distance_method
:members:
:undoc-members:
:show-inheritance:

knarrow.main module
-------------------

Expand All @@ -20,6 +36,14 @@ knarrow.main module
:undoc-members:
:show-inheritance:

knarrow.menger module
---------------------

.. automodule:: knarrow.menger
:members:
:undoc-members:
:show-inheritance:

knarrow.ols module
------------------

Expand Down
24 changes: 24 additions & 0 deletions src/knarrow/angle_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import numpy as np


def angle(x, y, **kwargs):
"""
Find a knee by looking at the maximum change of the angle of the line going through consecutive point pairs
Quite sensitive to noise, use with cubic spline smoothing.
Args:
x: npt.NDArray, the x coordinates of the points
y: npt.NDArray, the y coordinates of the points
**kwargs: possible additional arguments (none are used)
Returns: int, the index of the knee
"""
assert len(kwargs) == 0
assert x.shape == y.shape
d_x = np.diff(x)
d_y = np.diff(y)
angles = np.arctan2(d_y, d_x)
angle_differences = np.abs(np.diff(angles))
max_diff = angle_differences.argmax().item()
return max_diff + 1
6 changes: 6 additions & 0 deletions src/knarrow/angle_method.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import Any

import numpy as np
import numpy.typing as npt

def angle(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], kwargs: Any) -> int: ...
47 changes: 47 additions & 0 deletions src/knarrow/distance_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import numpy as np

from .util import np_windowed, projection_distance


def distance(x, y, **kwargs):
"""
Find a knee by finding a point which is most distant from the line y=x (after normalizing the inputs)
Fun fact: *vertical* distance of a point P from line y=x is just a scaled version of the *orthogonal* distance of
the same point P from line x=y.
Args:
x: np.ndarray, the x coordinates of the points
y: np.ndarray, the y coordinates of the points
**kwargs: possible additional arguments (none are actually used)
Returns: int, the index of the knee
"""
assert len(kwargs) == 0
assert x.shape == y.shape
distances = abs(y - x)
return np.argmax(distances).item()


def distance_adjacent(x, y, **kwargs):
"""
Find a knee by finding a point which is most distant from the line going through the neighbouring points.
Note: I developed a (not so) fancy linear algebra implementation so this should be quite fast. However, this method
is quite sensitive to noise, so only use with cubic spline smoothing.
Args:
x: np.ndarray, the x coordinates of the points
y:, np.ndarray, the y coordinates of the points
**kwargs: possible additional arguments (none are actually used)
Returns: int, the index of the knee
"""
assert len(kwargs) == 0
indices = np_windowed(len(x), 3)
x_windowed = x[indices] # shape = (len(x), 3)
y_windowed = y[indices] # shape = (len(x), 3)
points = np.stack((x_windowed, y_windowed), axis=-1) # shape = (len(x), 3, 2)
translated_points = points - points[:, [0], :] # anchor all the triplets at the origin. The list is important!
translated_points = translated_points[..., 1:, :] # remove the origin point, now shape = (len(x), 2, 2)
distances = projection_distance(translated_points)
return np.argmax(distances).item()
7 changes: 7 additions & 0 deletions src/knarrow/distance_method.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from typing import Any

import numpy as np
import numpy.typing as npt

def distance(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], kwargs: Any) -> int: ...
def distance_adjacent(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], kwargs: Any) -> int: ...
152 changes: 7 additions & 145 deletions src/knarrow/main.py
Original file line number Diff line number Diff line change
@@ -1,147 +1,9 @@
import numpy as np
from numpy import linalg as la
import numpy.typing as npt

from .angle_method import angle # noqa
from .c_method import c_method # noqa
from .distance_method import distance, distance_adjacent # noqa
from .menger import menger_anchored, menger_successive # noqa
from .ols import ols_swiping # noqa
from .util import np_anchored, np_windowed, prepare, projection_distance


def double_triangle_area(vertices):
"""
Return twice the area of a triangle with the given vertices.
Args:
vertices: npt.NDArray, 3x2 matrix where every row is a new 2-D vertex (x, y)
Returns:
double_area: npt.NDArray, 1x1 double of the area
"""
assert vertices.shape == (3, 2)
double_area: npt.NDArray[np.float_] = abs(la.det(np.hstack((vertices, np.ones((3, 1), dtype=vertices.dtype)))))
return double_area


def get_squared_vector_lengths(vertices):
"""
Return the square of the lengths between neighbouring vertices
Args:
vertices: npt.NDArray, array of vertices
Returns:
lengths: npt.NDArray, the entry at `lenghts[i]` is a squared distance between the vertex `i` and `i+1`
"""
vector_differences = np.empty(vertices.shape)

rolled = np.roll(vertices, -1, axis=0)
np.subtract(rolled, vertices, out=vector_differences)
lengths: npt.NDArray[np.float_] = np.einsum("ij,ij->i", vector_differences, vector_differences)
return lengths


def get_curvature(vertices):
"""
Calculate the Menger curvature defined by the three points
Args:
vertices: npt.NDArray, 3x2 matrix where every row is a new 2-D vertex (x, y)
Returns:
curvature: npt.NDArray, the reciprocal of the radius of the circumcircle around the vertices
"""
area = double_triangle_area(vertices)
value = 4 * area * area / np.prod(get_squared_vector_lengths(vertices))
curvature: npt.NDArray[np.float_] = np.sqrt(value)
return curvature


def menger_successive(x, y, **kwargs):
"""
Find a knee using the Menger curvature on the three successive points
Args:
x: npt.NDArray, the x coordinates of the points
y: npt.NDArray, the y coordinates of the points
**kwargs: possible additional arguments (none are used)
Returns: int, the index of the knee
"""
assert len(kwargs) == 0
assert x.shape == y.shape
indices = np_windowed(len(x), 3)
data_points = np.stack((x[indices], y[indices]), axis=-1)
curve_scores = np.array([get_curvature(row) for row in data_points])
return curve_scores.argmax().item() + 1


def menger_anchored(x, y, **kwargs):
"""
Find a knee using the Menger curvature on the first point, last point, and varying the middle point.
More resistant to the noise in the data than menger_successive
Args:
x: npt.NDArray, the x coordinates of the points
y: npt.NDArray, the y coordinates of the points
**kwargs: possible additional arguments (none are used)
e
Returns: int, the index of the knee
"""
assert len(kwargs) == 0
assert x.shape == y.shape
# perhaps later `menger_anchored` and `menger_successive` can be united in the future
# since the only difference is this line
indices = np_anchored(len(x))
data_points = np.stack((x[indices], y[indices]), axis=-1)
curve_scores = np.array([get_curvature(row) for row in data_points])
return curve_scores.argmax().item() + 1


def angle(x, y, **kwargs):
"""
Find a knee by looking at the maximum change of the angle between neighbouring points
Args:
x: npt.NDArray, the x coordinates of the points
y: npt.NDArray, the y coordinates of the points
**kwargs: possible additional arguments (none are used)
Returns: int, the index of the knee
"""
assert len(kwargs) == 0
assert x.shape == y.shape
d_x = np.diff(x)
d_y = np.diff(y)
angles = np.abs(np.arctan2(d_y, d_x))
angle_differences = np.diff(angles)
max_diff = angle_differences.argmax().item()
return max_diff + 1


def distance(x, y, **kwargs):
"""
Find a knee by finding a point which is most distant from the line y=x (after normalizing the inputs)
Fun fact: *vertical* distance of a point P from line y=x is just a scaled version of the *orthogonal* distance of
the same point P from line x=y.
Args:
x: npt.NDArray, the x coordinates of the points
y: npt.NDArray, the y coordinates of the points
**kwargs: possible additional arguments (non are used)
Returns: int, the index of the knee
"""
assert len(kwargs) == 0
assert x.shape == y.shape
distances = abs(y - x)
return np.argmax(distances).item()


def distance_adjacent(x, y, **kwargs):
assert len(kwargs) == 0
indices = np_windowed(len(x), 3)
x_windowed = x[indices] # shape = (len(x), 3)
y_windowed = y[indices] # shape = (len(x), 3)
points = np.stack((x_windowed, y_windowed), axis=-1) # shape = (len(x), 3, 2)
translated_points = points - points[:, [0], :] # anchor all the triplets at the origin. The list is important!
translated_points = translated_points[..., 1:, :] # remove the origin point, now shape = (len(x), 2, 2)
distances = projection_distance(translated_points)
return np.argmax(distances).item()
from .util import prepare


@prepare
Expand All @@ -158,12 +20,12 @@ def find_knee(x, y, method="menger_successive", **kwargs):
Returns: int, the index of the knee
"""
assert method in [
"menger_successive",
"menger_anchored",
"angle",
"distance",
"c_method",
"distance",
"distance_adjacent",
"menger_anchored",
"menger_successive",
"ols_swiping",
]
function = globals()[method]
Expand Down
7 changes: 0 additions & 7 deletions src/knarrow/main.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,4 @@ from typing import Any
import numpy as np
from numpy import typing as npt

def double_triangle_area(vertices: npt.NDArray[np.float_]) -> npt.NDArray[np.float_]: ...
def get_squared_vector_lengths(vertices: npt.NDArray[np.float_]) -> npt.NDArray[np.float_]: ...
def get_curvature(vertices: npt.NDArray[np.float_]) -> npt.NDArray[np.float_]: ...
def menger_successive(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], kwargs: Any) -> int: ...
def angle(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], kwargs: Any) -> int: ...
def distance(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], kwargs: Any) -> int: ...
def menger_anchored(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], kwargs: Any) -> int: ...
def find_knee(x: npt.NDArray[np.float_], y: npt.NDArray[np.float_], method: str, kwargs: Any) -> int: ...
Loading

0 comments on commit 088e54b

Please sign in to comment.