scrollstats 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ """
2
+ Copyright (c) 2025 Andrew Vanderheiden. All rights reserved.
3
+
4
+ scrollstats: An open-source python library to calculate and extract morphometrics from scroll bar floodplains
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ._version import version as __version__
10
+
11
+ __all__ = [
12
+ "BendDataExtractor",
13
+ "LineSmoother",
14
+ "MultiTransect",
15
+ "RidgeDataExtractor",
16
+ "TransectDataExtractor",
17
+ "__version__",
18
+ "calc_ridge_amps",
19
+ "calculate_ridge_metrics",
20
+ "create_ridge_area_raster",
21
+ "create_ridge_area_raster_fs",
22
+ "create_transects",
23
+ "map_amp_values",
24
+ ]
25
+
26
+ from .delineation import (
27
+ LineSmoother,
28
+ create_ridge_area_raster,
29
+ create_ridge_area_raster_fs,
30
+ )
31
+ from .ridge_metrics import (
32
+ BendDataExtractor,
33
+ RidgeDataExtractor,
34
+ TransectDataExtractor,
35
+ calc_ridge_amps,
36
+ calculate_ridge_metrics,
37
+ map_amp_values,
38
+ )
39
+ from .transecting import MultiTransect, create_transects
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.1'
32
+ __version_tuple__ = version_tuple = (0, 1, 1)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ version: str
4
+ version_tuple: tuple[int, int, int] | tuple[int, int, int, str, str]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = [
4
+ "LineSmoother",
5
+ "create_ridge_area_raster",
6
+ "create_ridge_area_raster_fs",
7
+ "quadratic_profile_curvature",
8
+ "residual_topography",
9
+ ]
10
+
11
+ from .line_smoother import LineSmoother
12
+ from .raster_classifiers import quadratic_profile_curvature, residual_topography
13
+ from .ridge_area_raster import create_ridge_area_raster, create_ridge_area_raster_fs
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, TypeAlias
5
+
6
+ from nptyping import Bool, Float, NDArray, Shape
7
+
8
+ # Define array types
9
+
10
+ ## Array2D: 2D array of any size of any dtype
11
+ Array2D: TypeAlias = NDArray[Shape["*, *"], Any] # noqa: F722 pylint: disable=C0103
12
+
13
+ ## ElevationArray2D: 2D array of any size of dtype float
14
+ ElevationArray2D: TypeAlias = NDArray[Shape["*, *"], Float] # noqa: F722 pylint: disable=C0103
15
+
16
+ ## BinaryArray2D: 2D array of any size of dtype bool
17
+ BinaryArray2D: TypeAlias = NDArray[Shape["*, *"], Bool] # noqa: F722 pylint: disable=C0103
18
+
19
+ # Define functions as interfaces
20
+ ## `BinaryClassifierFn`s and `BinaryDenoiserFn`s use the following signature:
21
+ ### They take a 2D array as input and output a binary 2D array
22
+ ### Use partial functions from the `functools` library to create wrapper functions which contain all input arguments other than the input 2D array
23
+
24
+ # Mypy does not allow for the complex Callable typing that's currently used (a callable with one typed input and any number of other inputs of any type)
25
+ ## So, the difference between a BinaryClassifierFn and BinaryDenoiserFn are in name only
26
+
27
+ # Takes an ElevationArray2D and kwargs as input
28
+ BinaryClassifierFn: TypeAlias = Callable[..., BinaryArray2D]
29
+
30
+ # Takes a BinaryArray2D and kwargs as input
31
+ BinaryDenoiserFn: TypeAlias = Callable[..., BinaryArray2D]
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ from geopandas import GeoDataFrame
5
+ from scipy.interpolate import CubicSpline
6
+ from shapely.geometry import LineString
7
+
8
+
9
+ class LineSmoother:
10
+ """
11
+ Smooth and densify rough, manually drawn LineStrings.
12
+
13
+ Smoothing is accomplished with the use of a mean filter and densifying is accomplished with the use of a piecewise cubic spline.
14
+ The GeoDataFrame provided must only contain LineStrings. MuliLineStrings or other geometries are not supported.
15
+ The vertex count of any ridge cannot be lower than the window size for the mean filter
16
+
17
+ Values used for the Lower Brazos Ridges were:
18
+ window = 5 (vertices)
19
+ spacing = 1 (meters)
20
+ """
21
+
22
+ def __init__(self, lines: GeoDataFrame, spacing: int, window: int) -> None:
23
+ self.lines = lines
24
+ self.spacing = spacing
25
+ self.window = window
26
+
27
+ # Perform checks on inputs
28
+ self.check_geometry_type()
29
+ self.check_vertex_count()
30
+
31
+ def check_geometry_type(self) -> None:
32
+ """Check that all geometries are of type LineString"""
33
+ if any(self.lines.geom_type != "LineString"):
34
+ err_msg = "Not all geometries are of type LineString. Remove the non-LineString geometry from `lines`"
35
+ raise ValueError(err_msg)
36
+
37
+ def check_vertex_count(self) -> None:
38
+ """Check that all ridges have at least as many vertices as the smoothing window is long"""
39
+ if any(self.lines.geometry.apply(lambda x: len(x.coords)) < self.window):
40
+ err_msg = f"One or more ridges have fewer vertices than the smoothing window is long (window={self.window}). Remove these ridges or add more vertices to them."
41
+ raise ValueError(err_msg)
42
+
43
+ def meanfilt(self, line: LineString, w: int) -> LineString:
44
+ """
45
+ Use a mean filter to smooth the xy points of the line.
46
+ This is done by passing a moving window with size `w` over the x and y coordinates separately and replacing the central value of the window with the mean value of the window.
47
+ This particular method appends the first and last coord to the new line to account for erosion via convolution
48
+ """
49
+
50
+ mode = "valid"
51
+ x, y = line.xy
52
+
53
+ xm = np.convolve(x, np.ones(w) * 1 / w, mode=mode)
54
+ xm = np.insert(xm, 0, x[0])
55
+ xm = np.append(xm, x[-1])
56
+
57
+ ym = np.convolve(y, np.ones(w) * 1 / w, mode=mode)
58
+ ym = np.insert(ym, 0, y[0])
59
+ ym = np.append(ym, y[-1])
60
+
61
+ return LineString(zip(xm, ym, strict=False))
62
+
63
+ def calc_dist(self, x: np.ndarray, y: np.ndarray) -> list[float]:
64
+ """Calc distance along the line"""
65
+ xdiff = np.ediff1d(x)
66
+ ydiff = np.ediff1d(y)
67
+
68
+ return list(
69
+ np.cumsum(
70
+ np.insert(np.sqrt(np.add(np.power(xdiff, 2), np.power(ydiff, 2))), 0, 0)
71
+ )
72
+ )
73
+
74
+ def calc_cubic_spline(self, line: LineString, spacing: int) -> LineString:
75
+ """
76
+ Fit a cubic spline function to a LineString then sample that function at the given `spacing`
77
+ """
78
+
79
+ # Get x,y values from LineString
80
+ x, y = line.xy
81
+
82
+ # Calc distance along the line
83
+ s = self.calc_dist(x, y)
84
+
85
+ # Get the total length of the line
86
+ length = s[-1]
87
+
88
+ # Total number of output points
89
+ n = int(length // spacing)
90
+
91
+ # Interpolated distances for each output point
92
+ interp_dist = np.linspace(0, length, n + 1, endpoint=True)
93
+
94
+ # Create spline function of x and y
95
+ cx_func = CubicSpline(s, x)
96
+ cy_func = CubicSpline(s, y)
97
+
98
+ # Apply Spline
99
+ cx = cx_func(interp_dist)
100
+ cy = cy_func(interp_dist)
101
+
102
+ return LineString(zip(cx, cy, strict=False))
103
+
104
+ def execute(self) -> GeoDataFrame:
105
+ """
106
+ Apply the mean filter and cubic spline to each line in the geodataframe.
107
+ Return a new geodataframe with the smooth lines
108
+ """
109
+ # Create a copy of the lines GeoDataFrame
110
+ out_lines = self.lines.copy()
111
+
112
+ # Smooth points with mean filter
113
+ out_lines.geometry = out_lines.geometry.apply(
114
+ lambda x: self.meanfilt(x, self.window)
115
+ )
116
+
117
+ # Densify points with cubic spline
118
+ out_lines.geometry = out_lines.geometry.apply(
119
+ lambda x: self.calc_cubic_spline(x, self.spacing)
120
+ )
121
+
122
+ return out_lines