soiltextureplot 0.1.0__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.
- soiltextureplot/__init__.py +32 -0
- soiltextureplot/classifier.py +95 -0
- soiltextureplot/datasets.py +106 -0
- soiltextureplot/plotting.py +247 -0
- soiltextureplot/systems.py +80 -0
- soiltextureplot/triangle.py +178 -0
- soiltextureplot/utils.py +85 -0
- soiltextureplot-0.1.0.dist-info/METADATA +106 -0
- soiltextureplot-0.1.0.dist-info/RECORD +12 -0
- soiltextureplot-0.1.0.dist-info/WHEEL +5 -0
- soiltextureplot-0.1.0.dist-info/licenses/LICENSE +21 -0
- soiltextureplot-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Soil Texture Plotting Package
|
|
3
|
+
=============================
|
|
4
|
+
|
|
5
|
+
A Python package for soil texture classification and plotting on ternary diagrams.
|
|
6
|
+
|
|
7
|
+
Modules
|
|
8
|
+
-------
|
|
9
|
+
classifier
|
|
10
|
+
Logic for classifying soil samples into texture classes.
|
|
11
|
+
datasets
|
|
12
|
+
Loading sample datasets.
|
|
13
|
+
plotting
|
|
14
|
+
Functions for creating ternary plots.
|
|
15
|
+
systems
|
|
16
|
+
Definitions of standard soil texture classification systems (USDA, HYPRES, etc.).
|
|
17
|
+
triangle
|
|
18
|
+
Core triangle geometry utilities.
|
|
19
|
+
utils
|
|
20
|
+
General utility functions.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .classifier import PolygonClassifier
|
|
24
|
+
from .systems import TextureSystem, get_texture_system
|
|
25
|
+
from .plotting import plot_triangle_with_points
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"PolygonClassifier",
|
|
29
|
+
"TextureSystem",
|
|
30
|
+
"get_texture_system",
|
|
31
|
+
"plot_triangle_with_points",
|
|
32
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from matplotlib.path import Path
|
|
6
|
+
|
|
7
|
+
from .systems import TextureSystem
|
|
8
|
+
from .utils import ternary_to_cartesian
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PolygonClassifier:
|
|
13
|
+
"""
|
|
14
|
+
Classifies points into soil texture classes using polygon inclusion.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
system : TextureSystem
|
|
19
|
+
The texture system definition containing polygon vertices.
|
|
20
|
+
_paths : Dict[str, Path]
|
|
21
|
+
Precomputed matplotlib Paths for point testing.
|
|
22
|
+
_class_order : List[str]
|
|
23
|
+
Ordered list of class names for consistency.
|
|
24
|
+
"""
|
|
25
|
+
system: TextureSystem
|
|
26
|
+
_paths: Dict[str, Path]
|
|
27
|
+
_class_order: List[str]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_system(cls, system: TextureSystem) -> "PolygonClassifier":
|
|
31
|
+
"""
|
|
32
|
+
Create a classifier from a TextureSystem.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
system : TextureSystem
|
|
37
|
+
The texture classification system to use.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
PolygonClassifier
|
|
42
|
+
Initialized classifier instance.
|
|
43
|
+
"""
|
|
44
|
+
paths: Dict[str, Path] = {}
|
|
45
|
+
|
|
46
|
+
for name, vertices in system.polygons.items():
|
|
47
|
+
verts = np.array(vertices, dtype=float) # shape (N, 3) (clay, sand, silt)
|
|
48
|
+
clay, sand, silt = verts.T
|
|
49
|
+
xy = ternary_to_cartesian(clay, sand, silt)
|
|
50
|
+
# Ensure polygon is closed
|
|
51
|
+
if not np.allclose(xy[0], xy[-1]):
|
|
52
|
+
xy = np.vstack([xy, xy[0]])
|
|
53
|
+
|
|
54
|
+
paths[name] = Path(xy)
|
|
55
|
+
|
|
56
|
+
class_order = list(system.polygons.keys())
|
|
57
|
+
return cls(system=system, _paths=paths, _class_order=class_order)
|
|
58
|
+
|
|
59
|
+
def classify_points(
|
|
60
|
+
self,
|
|
61
|
+
clay: np.ndarray,
|
|
62
|
+
sand: np.ndarray,
|
|
63
|
+
silt: np.ndarray
|
|
64
|
+
) -> np.ndarray:
|
|
65
|
+
"""
|
|
66
|
+
Classify many points at once.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
clay : np.ndarray
|
|
71
|
+
Array of clay percentages.
|
|
72
|
+
sand : np.ndarray
|
|
73
|
+
Array of sand percentages.
|
|
74
|
+
silt : np.ndarray
|
|
75
|
+
Array of silt percentages.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
np.ndarray
|
|
80
|
+
Array of class names (dtype=object). Returns 'Unknown' if no
|
|
81
|
+
polygon contains the point.
|
|
82
|
+
"""
|
|
83
|
+
xy = ternary_to_cartesian(clay, sand, silt)
|
|
84
|
+
n = xy.shape[0]
|
|
85
|
+
result = np.full(n, "Unknown", dtype=object)
|
|
86
|
+
|
|
87
|
+
# Vectorized point-in-polygon: test all points against each Path
|
|
88
|
+
for class_name in self._class_order:
|
|
89
|
+
path = self._paths[class_name]
|
|
90
|
+
inside = path.contains_points(xy)
|
|
91
|
+
# Only overwrite where still Unknown
|
|
92
|
+
mask = inside & (result == "Unknown")
|
|
93
|
+
result[mask] = class_name
|
|
94
|
+
|
|
95
|
+
return result
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import Dict, List, Annotated
|
|
2
|
+
|
|
3
|
+
# Type alias for texture definitions: Name -> List of Polygon Vertices (Clay, Sand, Silt)
|
|
4
|
+
TextureClasses = Dict[str, List[List[float]]]
|
|
5
|
+
|
|
6
|
+
USDA_TEXTURE_CLASSES: TextureClasses = {
|
|
7
|
+
"sand": [[10.0, 90.0, 0.0], [0.0, 100.0, 0.0], [0.0, 85.0, 15.0]],
|
|
8
|
+
"loamy sand": [
|
|
9
|
+
[15.0, 85.0, 0.0],
|
|
10
|
+
[10.0, 90.0, 0.0],
|
|
11
|
+
[0.0, 85.0, 15.0],
|
|
12
|
+
[0.0, 70.0, 30.0],
|
|
13
|
+
],
|
|
14
|
+
"sandy loam": [
|
|
15
|
+
[20.0, 52.0, 28.0],
|
|
16
|
+
[20.0, 80.0, 0.0],
|
|
17
|
+
[15.0, 85.0, 0.0],
|
|
18
|
+
[0.0, 70.0, 30.0],
|
|
19
|
+
[0.0, 50.0, 50.0],
|
|
20
|
+
[7.0, 43.0, 50.0],
|
|
21
|
+
[7.0, 52.0, 41.0],
|
|
22
|
+
],
|
|
23
|
+
"loam": [
|
|
24
|
+
[27.0, 23.0, 50.0],
|
|
25
|
+
[27.0, 45.0, 28.0],
|
|
26
|
+
[20.0, 52.0, 28.0],
|
|
27
|
+
[7.0, 52.0, 41.0],
|
|
28
|
+
[7.0, 43.0, 50.0],
|
|
29
|
+
],
|
|
30
|
+
"silt loam": [
|
|
31
|
+
[27.0, 0.0, 73.0],
|
|
32
|
+
[27.0, 23.0, 50.0],
|
|
33
|
+
[0.0, 50.0, 50.0],
|
|
34
|
+
[0.0, 20.0, 80.0],
|
|
35
|
+
[12.0, 8.0, 80.0],
|
|
36
|
+
[12.0, 0.0, 88.0],
|
|
37
|
+
],
|
|
38
|
+
"silt": [
|
|
39
|
+
[12.0, 0.0, 88.0],
|
|
40
|
+
[12.0, 8.0, 80.0],
|
|
41
|
+
[0.0, 20.0, 80.0],
|
|
42
|
+
[0.0, 0.0, 100.0],
|
|
43
|
+
],
|
|
44
|
+
"sandy clay loam": [
|
|
45
|
+
[35.0, 45.0, 20.0],
|
|
46
|
+
[35.0, 65.0, 0.0],
|
|
47
|
+
[20.0, 80.0, 0.0],
|
|
48
|
+
[20.0, 52.0, 28.0],
|
|
49
|
+
[27.0, 45.0, 28.0],
|
|
50
|
+
],
|
|
51
|
+
"clay loam": [
|
|
52
|
+
[40.0, 20.0, 40.0],
|
|
53
|
+
[40.0, 45.0, 15.0],
|
|
54
|
+
[27.0, 45.0, 28.0],
|
|
55
|
+
[27.0, 20.0, 53.0],
|
|
56
|
+
],
|
|
57
|
+
"silty clay loam": [
|
|
58
|
+
[40.0, 0.0, 60.0],
|
|
59
|
+
[40.0, 20.0, 40.0],
|
|
60
|
+
[27.0, 20.0, 53.0],
|
|
61
|
+
[27.0, 0.0, 73.0],
|
|
62
|
+
],
|
|
63
|
+
"sandy clay": [[55.0, 45.0, 0.0], [35.0, 65.0, 0.0], [35.0, 45.0, 20.0]],
|
|
64
|
+
"silty clay": [[60.0, 0.0, 40.0], [40.0, 20.0, 40.0], [40.0, 0.0, 60.0]],
|
|
65
|
+
"clay": [
|
|
66
|
+
[100.0, 0.0, 0.0],
|
|
67
|
+
[55.0, 45.0, 0.0],
|
|
68
|
+
[40.0, 45.0, 15.0],
|
|
69
|
+
[40.0, 20.0, 40.0],
|
|
70
|
+
[60.0, 0.0, 40.0],
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
HYPRES_TEXTURE_CLASSES: TextureClasses = {
|
|
75
|
+
"coarse": [ # Coarse: 0 ≤ clay < 18, sand ≥ 65
|
|
76
|
+
[18.0, 82.0, 0.0],
|
|
77
|
+
[0.0, 100.0, 0.0],
|
|
78
|
+
[0.0, 65.0, 35.0],
|
|
79
|
+
[18.0, 65.0, 17.0],
|
|
80
|
+
],
|
|
81
|
+
"medium": [ # Medium: (0–18 clay & 15–65 sand) OR (18–35 clay & sand ≥15)
|
|
82
|
+
[35.0, 65.0, 0.0],
|
|
83
|
+
[18.0, 82.0, 0.0],
|
|
84
|
+
[18.0, 65.0, 17.0],
|
|
85
|
+
[0.0, 65.0, 35.0],
|
|
86
|
+
[0.0, 15.0, 85.0],
|
|
87
|
+
[35.0, 15.0, 50.0],
|
|
88
|
+
],
|
|
89
|
+
"medium fine": [ # Medium fine: (clay<35) and (sand<15)
|
|
90
|
+
[35.0, 15.0, 50.0],
|
|
91
|
+
[0.0, 15.0, 85.0],
|
|
92
|
+
[0.0, 0.0, 100.0],
|
|
93
|
+
[35.0, 0.0, 65.0],
|
|
94
|
+
],
|
|
95
|
+
"fine": [ # Fine: 35 ≤ clay < 60
|
|
96
|
+
[60.0, 40.0, 0.0],
|
|
97
|
+
[35.0, 65.0, 0.0],
|
|
98
|
+
[35.0, 0.0, 65.0],
|
|
99
|
+
[60.0, 0.0, 40.0],
|
|
100
|
+
],
|
|
101
|
+
"very fine": [ # Very fine: clay ≥ 60
|
|
102
|
+
[100.0, 0.0, 0.0],
|
|
103
|
+
[60.0, 40.0, 0.0],
|
|
104
|
+
[60.0, 0.0, 40.0],
|
|
105
|
+
],
|
|
106
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Union, List, Tuple
|
|
6
|
+
from matplotlib.figure import Figure
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
|
+
from matplotlib.colors import Colormap
|
|
9
|
+
from matplotlib.ticker import AutoMinorLocator, MultipleLocator
|
|
10
|
+
from matplotlib import colormaps
|
|
11
|
+
|
|
12
|
+
from .systems import TextureSystem
|
|
13
|
+
from .utils import calculate_centroid
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def plot_triangle_with_points(
|
|
17
|
+
df: pd.DataFrame,
|
|
18
|
+
system: TextureSystem,
|
|
19
|
+
size_by: Optional[str] = None,
|
|
20
|
+
size_min: Optional[float] = None,
|
|
21
|
+
size_max: Optional[float] = None,
|
|
22
|
+
show_labels: Optional[bool] = None,
|
|
23
|
+
cmap: Optional[Union[str, List[str], Colormap]] = None,
|
|
24
|
+
color_points: Optional[Union[str, List[str]]] = None,
|
|
25
|
+
) -> Tuple[Figure, Axes]:
|
|
26
|
+
"""
|
|
27
|
+
Plot soil texture data on a ternary diagram.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
df : pd.DataFrame
|
|
32
|
+
DataFrame with 'clay', 'sand', 'silt' columns.
|
|
33
|
+
system : TextureSystem
|
|
34
|
+
The soil texture classification system to use.
|
|
35
|
+
size_by : str, optional
|
|
36
|
+
Column name to use for sizing points.
|
|
37
|
+
size_min : float, optional
|
|
38
|
+
Minimum point size.
|
|
39
|
+
size_max : float, optional
|
|
40
|
+
Maximum point size.
|
|
41
|
+
show_labels : bool, optional
|
|
42
|
+
If True, show sample labels on points.
|
|
43
|
+
cmap : str, list, or Colormap, optional
|
|
44
|
+
Colormap for filling texture classes. Can be a matplotlib colormap name
|
|
45
|
+
or a list of colors. Defaults to 'Set3_r'.
|
|
46
|
+
color_points : str or list, optional
|
|
47
|
+
Color for the scattered points.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
fig : Figure
|
|
52
|
+
The matplotlib Figure object.
|
|
53
|
+
ax : Axes
|
|
54
|
+
The matplotlib Axes object (ternary projection).
|
|
55
|
+
"""
|
|
56
|
+
import mpltern # imported here so core doesn’t hard‑depend for non‑plot use
|
|
57
|
+
|
|
58
|
+
fig = plt.figure(figsize=(7, 6))
|
|
59
|
+
ax = fig.add_subplot(projection="ternary", ternary_sum=100.0)
|
|
60
|
+
|
|
61
|
+
_plot_background_classes(ax, system, cmap=cmap)
|
|
62
|
+
|
|
63
|
+
# coordinates in ternary order (clay, sand, silt)
|
|
64
|
+
t = df["clay"].to_numpy()
|
|
65
|
+
l = df["sand"].to_numpy()
|
|
66
|
+
r = df["silt"].to_numpy()
|
|
67
|
+
|
|
68
|
+
sizes = _compute_sizes(df, size_by, size_min, size_max)
|
|
69
|
+
|
|
70
|
+
ax.scatter(
|
|
71
|
+
t,
|
|
72
|
+
l,
|
|
73
|
+
r,
|
|
74
|
+
c=color_points,
|
|
75
|
+
alpha=0.7,
|
|
76
|
+
edgecolors="none",
|
|
77
|
+
s=sizes,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if show_labels and "sample_id" in df.columns:
|
|
81
|
+
for (_, row), tt, ll, rr in zip(df.iterrows(), t, l, r):
|
|
82
|
+
ax.text(
|
|
83
|
+
tt,
|
|
84
|
+
ll,
|
|
85
|
+
rr,
|
|
86
|
+
row["sample_id"],
|
|
87
|
+
fontsize=7,
|
|
88
|
+
ha="center",
|
|
89
|
+
va="center",
|
|
90
|
+
color="white",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
ax.set_title(f"{system.name} Soil Texture Triangle", weight="bold", pad=20)
|
|
94
|
+
fig.tight_layout()
|
|
95
|
+
return fig, ax
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _plot_background_classes(
|
|
99
|
+
ax: Axes,
|
|
100
|
+
system: TextureSystem,
|
|
101
|
+
cmap: Optional[Union[str, List[str], Colormap]] = None
|
|
102
|
+
) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Plots the texture class polygons in the background.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
ax : Axes
|
|
109
|
+
The ternary axes to plot on.
|
|
110
|
+
system : TextureSystem
|
|
111
|
+
The system containing polygons to plot.
|
|
112
|
+
cmap : str, list, or Colormap, optional
|
|
113
|
+
Colormap to use for filling polygons.
|
|
114
|
+
"""
|
|
115
|
+
from itertools import cycle
|
|
116
|
+
|
|
117
|
+
num_polygons = len(system.polygons)
|
|
118
|
+
|
|
119
|
+
colors = None
|
|
120
|
+
if isinstance(cmap, list):
|
|
121
|
+
colors = cmap
|
|
122
|
+
else:
|
|
123
|
+
if cmap is None:
|
|
124
|
+
cmap = "Set3_r" # default
|
|
125
|
+
try:
|
|
126
|
+
colormap = colormaps.get_cmap(cmap)
|
|
127
|
+
# Resample the colormap to have exactly the number of colors we need.
|
|
128
|
+
# Then, get the list of colors by calling the resampled map.
|
|
129
|
+
# This is a robust way that works for both continuous and discrete colormaps.
|
|
130
|
+
resampled_cmap = colormap.resampled(num_polygons)
|
|
131
|
+
colors = [resampled_cmap(i) for i in range(resampled_cmap.N)]
|
|
132
|
+
except (ValueError, KeyError):
|
|
133
|
+
print(
|
|
134
|
+
f"Warning: Colormap '{cmap}' not found. Falling back to default 'Set3_r'."
|
|
135
|
+
)
|
|
136
|
+
colormap = colormaps.get_cmap("Set3_r")
|
|
137
|
+
resampled_cmap = colormap.resampled(num_polygons)
|
|
138
|
+
colors = [resampled_cmap(i) for i in range(resampled_cmap.N)]
|
|
139
|
+
|
|
140
|
+
# Cycle through colors if not enough are provided for the polygons.
|
|
141
|
+
# This is a safeguard, though resampled() should give the correct number.
|
|
142
|
+
if len(colors) < num_polygons:
|
|
143
|
+
color_cycle = cycle(colors)
|
|
144
|
+
else:
|
|
145
|
+
color_cycle = colors # type: ignore
|
|
146
|
+
|
|
147
|
+
for (name, vertices), color in zip(system.polygons.items(), color_cycle):
|
|
148
|
+
tn0, tn1, tn2 = np.array(vertices).T # clay, sand, silt
|
|
149
|
+
patch = ax.fill(
|
|
150
|
+
tn0,
|
|
151
|
+
tn1,
|
|
152
|
+
tn2,
|
|
153
|
+
ec="k",
|
|
154
|
+
fc=color,
|
|
155
|
+
alpha=0.5,
|
|
156
|
+
)
|
|
157
|
+
centroid = calculate_centroid(patch[0].get_xy())
|
|
158
|
+
|
|
159
|
+
label = name.capitalize()
|
|
160
|
+
|
|
161
|
+
ax.text(
|
|
162
|
+
centroid[0],
|
|
163
|
+
centroid[1],
|
|
164
|
+
label,
|
|
165
|
+
ha="center",
|
|
166
|
+
va="center",
|
|
167
|
+
transform=ax.transData,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# ticks, grid, etc. (same as you already have)
|
|
171
|
+
ax.taxis.set_major_locator(MultipleLocator(10.0))
|
|
172
|
+
ax.laxis.set_major_locator(MultipleLocator(10.0))
|
|
173
|
+
ax.raxis.set_major_locator(MultipleLocator(10.0))
|
|
174
|
+
ax.taxis.set_minor_locator(AutoMinorLocator(2))
|
|
175
|
+
ax.laxis.set_minor_locator(AutoMinorLocator(2))
|
|
176
|
+
ax.raxis.set_minor_locator(AutoMinorLocator(2))
|
|
177
|
+
ax.grid(which="both", linewidth=0.4, color="gray", alpha=0.6)
|
|
178
|
+
|
|
179
|
+
# Add custom axis labels along edges
|
|
180
|
+
|
|
181
|
+
# Sand (%) – bottom edge, centered, below triangle
|
|
182
|
+
ax.text(
|
|
183
|
+
-5,
|
|
184
|
+
33,
|
|
185
|
+
33, # roughly mid bottom edge in (t,l,r) = (clay,sand,silt)
|
|
186
|
+
"Sand [%]",
|
|
187
|
+
ha="center",
|
|
188
|
+
va="top",
|
|
189
|
+
fontsize=12,
|
|
190
|
+
rotation=0,
|
|
191
|
+
transform=ax.transTernaryAxes, # use ternary coordinate classification_system
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Clay (%) – left edge, centered, rotated up
|
|
195
|
+
ax.text(
|
|
196
|
+
40,
|
|
197
|
+
40,
|
|
198
|
+
-8, # somewhere along left edge
|
|
199
|
+
"Clay [%]",
|
|
200
|
+
ha="center",
|
|
201
|
+
va="center",
|
|
202
|
+
fontsize=12,
|
|
203
|
+
rotation=60, # rotate along left edge
|
|
204
|
+
transform=ax.transTernaryAxes,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Silt (%) – right edge, centered, rotated down
|
|
208
|
+
ax.text(
|
|
209
|
+
40,
|
|
210
|
+
-8,
|
|
211
|
+
40, # somewhere along right edge
|
|
212
|
+
"Silt [%]",
|
|
213
|
+
ha="center",
|
|
214
|
+
va="center",
|
|
215
|
+
fontsize=12,
|
|
216
|
+
rotation=-60, # rotate along right edge
|
|
217
|
+
transform=ax.transTernaryAxes,
|
|
218
|
+
)
|
|
219
|
+
ax.taxis.set_ticks_position("tick2")
|
|
220
|
+
ax.laxis.set_ticks_position("tick2")
|
|
221
|
+
ax.raxis.set_ticks_position("tick2")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _compute_sizes(
|
|
225
|
+
df: pd.DataFrame,
|
|
226
|
+
size_by: Optional[str],
|
|
227
|
+
size_min: Optional[float],
|
|
228
|
+
size_max: Optional[float]
|
|
229
|
+
) -> Union[float, np.ndarray]:
|
|
230
|
+
"""Helper to compute point sizes based on a column."""
|
|
231
|
+
if size_min is None:
|
|
232
|
+
size_min = 20.0 # default fallback
|
|
233
|
+
if size_by is None or size_by not in df.columns:
|
|
234
|
+
return size_min
|
|
235
|
+
|
|
236
|
+
if size_max is None:
|
|
237
|
+
size_max = 100.0 # default fallback
|
|
238
|
+
|
|
239
|
+
vals = df[size_by].to_numpy().astype(float)
|
|
240
|
+
valid = np.isfinite(vals)
|
|
241
|
+
if not valid.any() or np.allclose(vals[valid], vals[valid][0]):
|
|
242
|
+
return np.full_like(vals, (size_min + size_max) / 2.0)
|
|
243
|
+
|
|
244
|
+
vmin = vals[valid].min()
|
|
245
|
+
vmax = vals[valid].max()
|
|
246
|
+
norm = (vals - vmin) / (vmax - vmin)
|
|
247
|
+
return size_min + norm * (size_max - size_min)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Dict, Mapping, Any, Optional
|
|
3
|
+
from . import datasets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class TextureSystem:
|
|
8
|
+
"""
|
|
9
|
+
Represents a soil texture classification system.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
name : str
|
|
14
|
+
The unique name of the system (e.g., 'USDA').
|
|
15
|
+
polygons : Mapping[str, Any]
|
|
16
|
+
A mapping of class names to polygon vertices.
|
|
17
|
+
meta : Mapping[str, Any]
|
|
18
|
+
Metadata about the system (description, citation, etc.).
|
|
19
|
+
"""
|
|
20
|
+
name: str
|
|
21
|
+
polygons: Mapping[str, Any]
|
|
22
|
+
meta: Mapping[str, Any]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_SYSTEMS: Dict[str, TextureSystem] = {
|
|
26
|
+
"USDA": TextureSystem(
|
|
27
|
+
name="USDA",
|
|
28
|
+
polygons=datasets.USDA_TEXTURE_CLASSES,
|
|
29
|
+
meta={
|
|
30
|
+
"description": "United States Department of Agriculture (USDA) Soil Texture Classification"
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
"HYPRES": TextureSystem(
|
|
34
|
+
name="HYPRES",
|
|
35
|
+
polygons=datasets.HYPRES_TEXTURE_CLASSES,
|
|
36
|
+
meta={
|
|
37
|
+
"description": "The HYdraulic PRoperties of European Soils (HYPRES) is a European framework for classifying soils based on their hydrologic properties"
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
# additional systems can be added here
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_texture_system(system_name: str) -> TextureSystem:
|
|
45
|
+
"""
|
|
46
|
+
Retrieve a TextureSystem by name.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
system_name : str
|
|
51
|
+
The name of the system to retrieve.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
TextureSystem
|
|
56
|
+
The requested texture system.
|
|
57
|
+
|
|
58
|
+
Raises
|
|
59
|
+
------
|
|
60
|
+
ValueError
|
|
61
|
+
If the system name is not found.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
return _SYSTEMS[system_name]
|
|
65
|
+
except KeyError:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Unknown texture system {system_name!r}. Available: {list(_SYSTEMS)}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def list_texture_systems() -> Dict[str, str]:
|
|
72
|
+
"""
|
|
73
|
+
List all available texture systems.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
Dict[str, str]
|
|
78
|
+
A dictionary mapping system names to their descriptions.
|
|
79
|
+
"""
|
|
80
|
+
return {k: v.meta.get("description", "") for k, v in _SYSTEMS.items()}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path as PathLibPath
|
|
6
|
+
from typing import Optional, Union, TYPE_CHECKING
|
|
7
|
+
from matplotlib.figure import Figure
|
|
8
|
+
from matplotlib.axes import Axes
|
|
9
|
+
|
|
10
|
+
from .systems import get_texture_system, TextureSystem
|
|
11
|
+
from .classifier import PolygonClassifier
|
|
12
|
+
from . import plotting
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
# Avoid circular import at runtime by only importing for type checking if needed
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SoilTextureTriangle:
|
|
21
|
+
"""
|
|
22
|
+
Main interface for loading soil data, classifying it, and plotting it.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
system_name : str, optional
|
|
27
|
+
Name of the texture classification system. Defaults to 'USDA'.
|
|
28
|
+
df : pd.DataFrame, optional
|
|
29
|
+
Initial DataFrame. Can be set later via load functions.
|
|
30
|
+
"""
|
|
31
|
+
system_name: str = "USDA"
|
|
32
|
+
df: Optional[pd.DataFrame] = field(default=None, repr=False)
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
self.system: TextureSystem = get_texture_system(self.system_name)
|
|
36
|
+
self._classifier = PolygonClassifier.from_system(self.system)
|
|
37
|
+
|
|
38
|
+
# data loading
|
|
39
|
+
def load_csv(
|
|
40
|
+
self,
|
|
41
|
+
path: Union[str, PathLibPath],
|
|
42
|
+
sand_col: str = "sand",
|
|
43
|
+
silt_col: str = "silt",
|
|
44
|
+
clay_col: str = "clay",
|
|
45
|
+
) -> "SoilTextureTriangle":
|
|
46
|
+
"""
|
|
47
|
+
Load data from a CSV file.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
path : str or Path
|
|
52
|
+
Path to the CSV file.
|
|
53
|
+
sand_col : str
|
|
54
|
+
Name of the column containing sand percentages.
|
|
55
|
+
silt_col : str
|
|
56
|
+
Name of the column containing silt percentages.
|
|
57
|
+
clay_col : str
|
|
58
|
+
Name of the column containing clay percentages.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
SoilTextureTriangle
|
|
63
|
+
Self for chaining.
|
|
64
|
+
"""
|
|
65
|
+
df = pd.read_csv(path)
|
|
66
|
+
return self.load_dataframe(df, sand_col, silt_col, clay_col)
|
|
67
|
+
|
|
68
|
+
def load_dataframe(
|
|
69
|
+
self,
|
|
70
|
+
df: pd.DataFrame,
|
|
71
|
+
sand_col: str = "sand",
|
|
72
|
+
silt_col: str = "silt",
|
|
73
|
+
clay_col: str = "clay",
|
|
74
|
+
) -> "SoilTextureTriangle":
|
|
75
|
+
"""
|
|
76
|
+
Load data from an existing DataFrame.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
df : pd.DataFrame
|
|
81
|
+
The DataFrame containing soil data.
|
|
82
|
+
sand_col : str
|
|
83
|
+
Name of the column containing sand percentages.
|
|
84
|
+
silt_col : str
|
|
85
|
+
Name of the column containing silt percentages.
|
|
86
|
+
clay_col : str
|
|
87
|
+
Name of the column containing clay percentages.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
SoilTextureTriangle
|
|
92
|
+
Self for chaining.
|
|
93
|
+
"""
|
|
94
|
+
# normalize column names internally
|
|
95
|
+
self.df = df.rename(
|
|
96
|
+
columns={sand_col: "sand", silt_col: "silt", clay_col: "clay"}
|
|
97
|
+
)
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
# classification
|
|
101
|
+
def classify(self) -> pd.DataFrame:
|
|
102
|
+
"""
|
|
103
|
+
Classify loaded data into texture classes.
|
|
104
|
+
|
|
105
|
+
Adds a 'texture_class' column to the internal DataFrame.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
pd.DataFrame
|
|
110
|
+
The DataFrame with the added 'texture_class' column.
|
|
111
|
+
|
|
112
|
+
Raises
|
|
113
|
+
------
|
|
114
|
+
ValueError
|
|
115
|
+
If no data has been loaded.
|
|
116
|
+
"""
|
|
117
|
+
if self.df is None:
|
|
118
|
+
raise ValueError("No data loaded. Call load_csv or load_dataframe first.")
|
|
119
|
+
|
|
120
|
+
clay = self.df["clay"].to_numpy()
|
|
121
|
+
sand = self.df["sand"].to_numpy()
|
|
122
|
+
silt = self.df["silt"].to_numpy()
|
|
123
|
+
|
|
124
|
+
classes = self._classifier.classify_points(clay, sand, silt)
|
|
125
|
+
self.df["texture_class"] = classes
|
|
126
|
+
return self.df
|
|
127
|
+
|
|
128
|
+
# plotting
|
|
129
|
+
def plot(
|
|
130
|
+
self,
|
|
131
|
+
size_by: Optional[str] = None,
|
|
132
|
+
size_min: float = 40,
|
|
133
|
+
size_max: float = 160,
|
|
134
|
+
show_labels: bool = True,
|
|
135
|
+
cmap: Optional[str] = None,
|
|
136
|
+
color_points: Optional[str] = "black",
|
|
137
|
+
) -> tuple[Figure, Axes]:
|
|
138
|
+
"""
|
|
139
|
+
Plot current data on the soil texture triangle using mpltern.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
size_by : str, optional
|
|
144
|
+
Column name for sizing points.
|
|
145
|
+
size_min : float, optional
|
|
146
|
+
Min point size.
|
|
147
|
+
size_max : float, optional
|
|
148
|
+
Max point size.
|
|
149
|
+
show_labels : bool, optional
|
|
150
|
+
Show labels on points.
|
|
151
|
+
cmap : str, optional
|
|
152
|
+
Colormap name for background polygons.
|
|
153
|
+
color_points : str, optional
|
|
154
|
+
Color for sample points.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
fig, ax
|
|
159
|
+
Matplotlib Figure and Axes.
|
|
160
|
+
|
|
161
|
+
Raises
|
|
162
|
+
------
|
|
163
|
+
ValueError
|
|
164
|
+
If no data is loaded.
|
|
165
|
+
"""
|
|
166
|
+
if self.df is None:
|
|
167
|
+
raise ValueError("No data loaded. Call load_csv or load_dataframe first.")
|
|
168
|
+
|
|
169
|
+
return plotting.plot_triangle_with_points(
|
|
170
|
+
df=self.df,
|
|
171
|
+
system=self.system,
|
|
172
|
+
size_by=size_by,
|
|
173
|
+
size_min=size_min,
|
|
174
|
+
size_max=size_max,
|
|
175
|
+
show_labels=show_labels,
|
|
176
|
+
cmap=cmap,
|
|
177
|
+
color_points=color_points,
|
|
178
|
+
)
|
soiltextureplot/utils.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def calculate_centroid(vertices: np.ndarray) -> np.ndarray:
|
|
5
|
+
"""
|
|
6
|
+
Compute centroid of a 2D polygon given an (N, 2) array of vertices.
|
|
7
|
+
Uses the standard shoelace formula (no np.cross).
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
vertices : np.ndarray
|
|
12
|
+
Array of shape (N, 2) containing polygon vertices.
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
np.ndarray
|
|
17
|
+
Array of shape (2,) containing the (x, y) centroid coordinates.
|
|
18
|
+
"""
|
|
19
|
+
x = vertices[:, 0]
|
|
20
|
+
y = vertices[:, 1]
|
|
21
|
+
|
|
22
|
+
# Close the polygon
|
|
23
|
+
x_next = np.roll(x, -1)
|
|
24
|
+
y_next = np.roll(y, -1)
|
|
25
|
+
|
|
26
|
+
cross = x * y_next - x_next * y
|
|
27
|
+
area = cross.sum() / 2.0
|
|
28
|
+
|
|
29
|
+
if np.isclose(area, 0.0):
|
|
30
|
+
# Fallback: simple mean if area is ~0 (degenerate polygon)
|
|
31
|
+
return np.array([x.mean(), y.mean()])
|
|
32
|
+
|
|
33
|
+
cx = ((x + x_next) * cross).sum() / (6.0 * area)
|
|
34
|
+
cy = ((y + y_next) * cross).sum() / (6.0 * area)
|
|
35
|
+
|
|
36
|
+
return np.array([cx, cy])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ternary_to_cartesian(
|
|
40
|
+
clay: np.ndarray,
|
|
41
|
+
sand: np.ndarray,
|
|
42
|
+
silt: np.ndarray,
|
|
43
|
+
ternary_sum: float = 100.0
|
|
44
|
+
) -> np.ndarray:
|
|
45
|
+
"""
|
|
46
|
+
Convert ternary coordinates (clay, sand, silt) to 2D Cartesian (x, y).
|
|
47
|
+
|
|
48
|
+
Uses an equilateral triangle with side length = ternary_sum.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
clay : np.ndarray
|
|
53
|
+
Clay percentages.
|
|
54
|
+
sand : np.ndarray
|
|
55
|
+
Sand percentages.
|
|
56
|
+
silt : np.ndarray
|
|
57
|
+
Silt percentages.
|
|
58
|
+
ternary_sum : float, optional
|
|
59
|
+
The sum of the ternary components (default 100.0).
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
np.ndarray
|
|
64
|
+
Array of shape (N, 2) containing Cartesian (x, y) coordinates.
|
|
65
|
+
"""
|
|
66
|
+
clay = np.asarray(clay, dtype=float)
|
|
67
|
+
sand = np.asarray(sand, dtype=float)
|
|
68
|
+
silt = np.asarray(silt, dtype=float)
|
|
69
|
+
|
|
70
|
+
# Normalize to sum to ternary_sum to be safe
|
|
71
|
+
total = clay + sand + silt
|
|
72
|
+
total[total == 0] = ternary_sum
|
|
73
|
+
clay = clay * ternary_sum / total
|
|
74
|
+
sand = sand * ternary_sum / total
|
|
75
|
+
silt = silt * ternary_sum / total
|
|
76
|
+
|
|
77
|
+
# Place triangle with one vertex at (0, 0), base horizontal
|
|
78
|
+
# Many ternary implementations use:
|
|
79
|
+
# x = 0.5 * (2*sand + silt) / ternary_sum
|
|
80
|
+
# y = (np.sqrt(3)/2) * silt / ternary_sum
|
|
81
|
+
# but we’ll keep units ~percent and let plotting handle scaling.
|
|
82
|
+
x = sand + 0.5 * silt
|
|
83
|
+
y = (np.sqrt(3) / 2.0) * silt
|
|
84
|
+
|
|
85
|
+
return np.stack([x, y], axis=-1)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: soiltextureplot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Soil texture triangle plotting and classification
|
|
5
|
+
Author-email: Amninder Singh <amnindersingh13@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Amninder Singh
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE OR ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/singhamninder/soiltextureplot
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/singhamninder/soiltextureplot/issues
|
|
30
|
+
Keywords: soil,texture,triangle,plot,classification,ternary
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Intended Audience :: Science/Research
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Scientific/Engineering
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Requires-Dist: pandas>=1.5.0
|
|
44
|
+
Requires-Dist: numpy>=1.23.0
|
|
45
|
+
Requires-Dist: matplotlib>=3.7.0
|
|
46
|
+
Requires-Dist: mpltern>=1.0.0
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# Soil Texture Plot
|
|
50
|
+
|
|
51
|
+
[](https://soiltextureplot.streamlit.app/)
|
|
52
|
+
|
|
53
|
+
This repository contains codebase for soil texture classification and visualization using ternary diagrams. It provides tools to plot soil texture data on texture triangles and classify soil samples according to different classification systems.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Ternary Plotting**: Visualize soil texture data on interactive ternary diagrams
|
|
58
|
+
- **Multiple Classification Systems**: Support for USDA and HYPRES soil texture classification systems
|
|
59
|
+
- **Interactive Web App**: Streamlit-based application for easy data upload and visualization
|
|
60
|
+
- **Flexible Data Input**: Support for CSV files with customizable column mapping
|
|
61
|
+
- **Point Classification**: Automatic classification of soil samples into texture classes
|
|
62
|
+
- **Customizable Visualization**: Control point sizes, colors, and labels
|
|
63
|
+
|
|
64
|
+
## Dependencies
|
|
65
|
+
|
|
66
|
+
- streamlit
|
|
67
|
+
- pandas
|
|
68
|
+
- numpy
|
|
69
|
+
- matplotlib
|
|
70
|
+
- mpltern
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Web Application
|
|
74
|
+
|
|
75
|
+
The web app allows you to:
|
|
76
|
+
- Upload CSV files with soil texture data
|
|
77
|
+
- Map columns to sand, silt, and clay percentages
|
|
78
|
+
- Visualize data on interactive texture triangles
|
|
79
|
+
- Customize plot appearance
|
|
80
|
+
|
|
81
|
+
## Supported Classification Systems
|
|
82
|
+
|
|
83
|
+
### USDA (United States Department of Agriculture)
|
|
84
|
+
The standard USDA soil texture classification system with 12 texture classes.
|
|
85
|
+
|
|
86
|
+
### HYPRES (HYdraulic PRoperties of European Soils)
|
|
87
|
+
A European framework for classifying soils based on their hydrologic properties.
|
|
88
|
+
|
|
89
|
+
## Data Format
|
|
90
|
+
|
|
91
|
+
Your CSV file should contain soil texture data with percentages of sand, silt, and clay. The percentages should sum to 100% for each sample.
|
|
92
|
+
|
|
93
|
+
Example data format:
|
|
94
|
+
|
|
95
|
+
```csv
|
|
96
|
+
sample_id,sand,silt,clay
|
|
97
|
+
S1,65,20,15
|
|
98
|
+
S2,70,24,6
|
|
99
|
+
S3,75,21,4
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Acknowledgments
|
|
103
|
+
|
|
104
|
+
- Built using [mpltern](https://github.com/yuzie007/mpltern) for ternary plotting
|
|
105
|
+
- Inspired by soil science classification standards
|
|
106
|
+
- Streamlit for the web interface
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
soiltextureplot/__init__.py,sha256=PXZ9KDE8bVO0seCW21ds3psztkh_Ab2mQdAyGf_YrbQ,774
|
|
2
|
+
soiltextureplot/classifier.py,sha256=sPf33xI5gJHKACTIWaqW9r1MPpX9R8VMRgsXJgzDt8M,2748
|
|
3
|
+
soiltextureplot/datasets.py,sha256=k42L-eMIlG_ehWwxW7Is_JUIWOAEgOgyIzYXRayp3RY,2827
|
|
4
|
+
soiltextureplot/plotting.py,sha256=sH9_2YKz9O0XVkJOhb5izf47yRk7_epaIioF9fOEsXU,7485
|
|
5
|
+
soiltextureplot/systems.py,sha256=sk4YFLjLlRbyAAPsitmBI3hG5mJq3bOhAIcOgUJm9K8,2046
|
|
6
|
+
soiltextureplot/triangle.py,sha256=lAySOgGZFipaTeqedoG4H2NU3d-x6C2VKkWdFeZYFQ8,5056
|
|
7
|
+
soiltextureplot/utils.py,sha256=Yhq3PXPUQt17xkaH1HEO9fvZHeR7E5UzH4VIcNUtaDc,2324
|
|
8
|
+
soiltextureplot-0.1.0.dist-info/licenses/LICENSE,sha256=heva5rpPPnoz6pdO3QinIy2P0LfIHXOoi_CALnus4f8,1073
|
|
9
|
+
soiltextureplot-0.1.0.dist-info/METADATA,sha256=OH2tw4o0u0hWAqI-1tBZIKK9ew6ea10ZnpDSxHKHU2A,4274
|
|
10
|
+
soiltextureplot-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
soiltextureplot-0.1.0.dist-info/top_level.txt,sha256=MyfpMMvqcQcnj28Ns9jfqGvl6zqo4vaycuPFu8oDXv0,16
|
|
12
|
+
soiltextureplot-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Amninder Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE OR ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
soiltextureplot
|