lsapy 0.1.0.dev0__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.
- lsapy/__init__.py +9 -0
- lsapy/criteria.py +72 -0
- lsapy/data/climate_sample.nc +0 -0
- lsapy/data/soil_sample.nc +0 -0
- lsapy/functions.py +266 -0
- lsapy/lsa.py +331 -0
- lsapy/statistics.py +109 -0
- lsapy/utils.py +28 -0
- lsapy-0.1.0.dev0.dist-info/METADATA +27 -0
- lsapy-0.1.0.dev0.dist-info/RECORD +12 -0
- lsapy-0.1.0.dev0.dist-info/WHEEL +4 -0
- lsapy-0.1.0.dev0.dist-info/licenses/LICENSE +19 -0
lsapy/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""A Python package for Land Suitability Analysis."""
|
|
2
|
+
|
|
3
|
+
from lsapy.lsa import LandSuitability
|
|
4
|
+
from lsapy.criteria import SuitabilityCriteria
|
|
5
|
+
from lsapy.functions import *
|
|
6
|
+
|
|
7
|
+
__author__ = "Baptiste Hamon"
|
|
8
|
+
__email__ = "baptiste.hamon@pg.canterbury.ac.nz"
|
|
9
|
+
__version__ = "0.1.0-dev0"
|
lsapy/criteria.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Suitability Criteria definition."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union, Any
|
|
4
|
+
import xarray as xr
|
|
5
|
+
|
|
6
|
+
from lsapy.functions import SuitabilityFunction, MembershipSuitFunction, DiscreteSuitFunction
|
|
7
|
+
|
|
8
|
+
__all__ = ["SuitabilityCriteria"]
|
|
9
|
+
|
|
10
|
+
class SuitabilityCriteria:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
name: str,
|
|
14
|
+
indicator: xr.Dataset | xr.DataArray,
|
|
15
|
+
func: Union[SuitabilityFunction, MembershipSuitFunction, DiscreteSuitFunction],
|
|
16
|
+
weight: Optional[Union[int,float]] = 1,
|
|
17
|
+
category: Optional[str] = None,
|
|
18
|
+
long_name: Optional[str] = None,
|
|
19
|
+
description: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
) -> None:
|
|
22
|
+
self.name = name
|
|
23
|
+
self.indicator = indicator
|
|
24
|
+
self.func = func
|
|
25
|
+
self.weight = weight
|
|
26
|
+
self.category = category
|
|
27
|
+
self.long_name = long_name
|
|
28
|
+
self.description = description
|
|
29
|
+
self._from_indicator = _get_indicator_description(indicator)
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
attrs = []
|
|
33
|
+
attrs.append(f"name='{self.name}'")
|
|
34
|
+
attrs.append(f"indicator={self.indicator.name}")
|
|
35
|
+
attrs.append(f"func={self.func}")
|
|
36
|
+
attrs.append(f"weight={self.weight}")
|
|
37
|
+
if self.category is not None:
|
|
38
|
+
attrs.append(f"category='{self.category}'")
|
|
39
|
+
if self.long_name is not None:
|
|
40
|
+
attrs.append(f"long_name='{self.long_name}'")
|
|
41
|
+
if self.description is not None:
|
|
42
|
+
attrs.append(f"description='{self.description}'")
|
|
43
|
+
return f"{self.__class__.__name__}({', '.join(attrs) if attrs else ''})"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute(self) -> xr.DataArray:
|
|
47
|
+
if self.func.func_method == 'discrete': # need to vectorize the discrete function
|
|
48
|
+
sc : xr.DataArray = xr.apply_ufunc(self.func.map, self.indicator).rename(self.name)
|
|
49
|
+
else:
|
|
50
|
+
sc : xr.DataArray = self.func.map(self.indicator).rename(self.name)
|
|
51
|
+
return sc.assign_attrs(dict({k: v for k, v in self.attrs.items() if k not in ['name', 'func_method', 'from_indicator']},
|
|
52
|
+
**{'history': f"func_method: {self.func}; from_indicator: [{self._from_indicator}]", 'compute': 'done'}))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def attrs(self):
|
|
57
|
+
return {k: v for k, v in {
|
|
58
|
+
'name': self.name,
|
|
59
|
+
'weight': self.weight,
|
|
60
|
+
'category': self.category,
|
|
61
|
+
'long_name': self.long_name,
|
|
62
|
+
'description': self.description,
|
|
63
|
+
'func_method': self.func,
|
|
64
|
+
'from_indicator': self._from_indicator
|
|
65
|
+
}.items() if v is not None}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_indicator_description(indicator: xr.Dataset | xr.DataArray) -> str:
|
|
69
|
+
if indicator.attrs != {}:
|
|
70
|
+
return f"name: {indicator.name}; " + "; ".join([f"{k}: {v}" for k, v in indicator.attrs.items()])
|
|
71
|
+
else:
|
|
72
|
+
return f"name: {indicator.name}"
|
|
Binary file
|
|
Binary file
|
lsapy/functions.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Suitability Functions definitions."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Callable, Union, Any
|
|
4
|
+
import warnings
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import xarray as xr
|
|
8
|
+
from scipy.optimize import curve_fit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"SuitabilityFunction",
|
|
13
|
+
"MembershipSuitFunction",
|
|
14
|
+
"DiscreteSuitFunction"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SuitabilityFunction:
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
func: Optional[Callable] = None,
|
|
23
|
+
func_method: Optional[str] = None,
|
|
24
|
+
func_params: Union[dict[str, Any]] = {}
|
|
25
|
+
):
|
|
26
|
+
if func_params is not None:
|
|
27
|
+
if func is None and func_method is None:
|
|
28
|
+
raise ValueError("If `func_params` is provided, `func` or `func_method` must also be provided.")
|
|
29
|
+
|
|
30
|
+
self.func = func
|
|
31
|
+
self.func_method = func_method
|
|
32
|
+
self.func_params = func_params
|
|
33
|
+
if func is None and func_method is not None:
|
|
34
|
+
self.func = _get_function_from_name(func_method)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
return f"{self.__class__.__name__}(func={self.func.__name__}, func_method='{self.func_method}', func_params={self.func_params})"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def __call__(self, x):
|
|
42
|
+
if self.func is None:
|
|
43
|
+
raise ValueError("No function has been provided.")
|
|
44
|
+
return self.func(x, **self.func_params)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def map(self, x):
|
|
48
|
+
return self(x)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def plot(self, x) -> None:
|
|
52
|
+
plt.plot(x, self(x))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def attrs(self):
|
|
57
|
+
if self.func_method == None and self.func_params == None:
|
|
58
|
+
return {}
|
|
59
|
+
return {k: v for k, v in {
|
|
60
|
+
'func_method': self.func_method,
|
|
61
|
+
'func_params': self.func_params
|
|
62
|
+
}.items() if v is not None}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------- #
|
|
66
|
+
# ------------------------ Membership functions ------------------------------ #
|
|
67
|
+
# ---------------------------------------------------------------------------- #
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MembershipSuitFunction(SuitabilityFunction):
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
func: Optional[Callable] = None,
|
|
75
|
+
func_method: Optional[str] = None,
|
|
76
|
+
func_params: Optional[dict[str, int | float]] = None
|
|
77
|
+
):
|
|
78
|
+
super().__init__(func, func_method, func_params)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def fit(x, y = np.array([0,.25,.5,.75,1]), methods: str | list[str] = 'all', plot: bool = False):
|
|
83
|
+
return _fit_mbs_functions(x, y, methods, plot)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _prepare_for_fitting(methods: str | list[str] = 'all'):
|
|
87
|
+
_types = ['sigmoid_like', 'gaussian_like']
|
|
88
|
+
_skipped = []
|
|
89
|
+
|
|
90
|
+
if methods == 'all':
|
|
91
|
+
methods = [f for t in _types for f in equations[t.replace('_like', '')]]
|
|
92
|
+
elif isinstance(methods, list) or isinstance(methods, str):
|
|
93
|
+
if isinstance(methods, str):
|
|
94
|
+
methods = [methods]
|
|
95
|
+
|
|
96
|
+
_methods = []
|
|
97
|
+
for method in methods:
|
|
98
|
+
if method in _types:
|
|
99
|
+
[_methods.append(m) for m in equations[method.replace('_like', '')].keys()]
|
|
100
|
+
else:
|
|
101
|
+
try :
|
|
102
|
+
_get_function_from_name(method)
|
|
103
|
+
_methods.append(method)
|
|
104
|
+
except :
|
|
105
|
+
_skipped.append(method)
|
|
106
|
+
warnings.warn(f"`{method}` not found in equations. Skipped.", stacklevel=2)
|
|
107
|
+
methods = _methods
|
|
108
|
+
for m in ['sigmoid', 'vetharaniam2024_eq8']:
|
|
109
|
+
if m in methods:
|
|
110
|
+
methods.remove(m)
|
|
111
|
+
_skipped.append(m)
|
|
112
|
+
if m == 'sigmoid':
|
|
113
|
+
warnings.warn("No parameters to determine for `sigmoid`. Skipped.", stacklevel=2)
|
|
114
|
+
if m == 'vetharaniam2024_eq8':
|
|
115
|
+
warnings.warn("Fitting method does not support `vetharaniam2024_eq8`. Skipped.", stacklevel=2)
|
|
116
|
+
return methods, _skipped
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_function_p0(method: str, x: np.ndarray) -> list[float]:
|
|
120
|
+
if method in equations['sigmoid']:
|
|
121
|
+
return [1, np.median(x)]
|
|
122
|
+
if method in equations['gaussian']:
|
|
123
|
+
return [1, np.median(x), 1]
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _fit_mbs_functions(x, y, methods: str | list[str] = 'all', plot: bool = False):
|
|
128
|
+
|
|
129
|
+
skipped = []
|
|
130
|
+
methods, _skipped = _prepare_for_fitting(methods)
|
|
131
|
+
skipped.extend(_skipped)
|
|
132
|
+
|
|
133
|
+
if len(methods) == 0:
|
|
134
|
+
print(f"Skipped fitting for the following methods: {', '.join(skipped)}.")
|
|
135
|
+
raise ValueError("No methods to fit.")
|
|
136
|
+
else :
|
|
137
|
+
x_ = np.linspace(min(x), max(x), 100)
|
|
138
|
+
rms_errors = []
|
|
139
|
+
f_params = []
|
|
140
|
+
for method in methods:
|
|
141
|
+
try :
|
|
142
|
+
f = _get_function_from_name(method)
|
|
143
|
+
p0 = _get_function_p0(method, x)
|
|
144
|
+
popt, _ = curve_fit(f, x, y, p0=p0, maxfev=15000)
|
|
145
|
+
y_ = f(x_, *popt)
|
|
146
|
+
f_params.append(popt)
|
|
147
|
+
rmse = _rms_error(y,f(x, *popt))
|
|
148
|
+
rms_errors.append(rmse)
|
|
149
|
+
if plot:
|
|
150
|
+
plt.plot(x_, y_, label=method + f' (RMSE={rmse:.2f})')
|
|
151
|
+
except:
|
|
152
|
+
skipped.append(method)
|
|
153
|
+
warnings.warn(f"Failed to fit `{method}`. Skipped.", stacklevel=2)
|
|
154
|
+
if plot:
|
|
155
|
+
plt.scatter(x, y, c='r')
|
|
156
|
+
plt.legend()
|
|
157
|
+
plt.show()
|
|
158
|
+
|
|
159
|
+
if len(skipped) > 0:
|
|
160
|
+
print(f"Skipped fitting for the following methods: {', '.join(skipped)}.")
|
|
161
|
+
f_best, p_best = _get_best_fit(methods, rms_errors, f_params)
|
|
162
|
+
return _get_function_from_name(f_best), p_best
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------- #
|
|
166
|
+
# --------------------------- Discrete functions ----------------------------- #
|
|
167
|
+
# ---------------------------------------------------------------------------- #
|
|
168
|
+
|
|
169
|
+
class DiscreteSuitFunction(SuitabilityFunction):
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
func_params: Optional[dict[str, int | float]] = None
|
|
174
|
+
):
|
|
175
|
+
self.func = discrete
|
|
176
|
+
self.func_method = 'discrete'
|
|
177
|
+
self.func_params = func_params
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------- #
|
|
181
|
+
# ---------------------------- Utility functions ----------------------------- #
|
|
182
|
+
# ---------------------------------------------------------------------------- #
|
|
183
|
+
|
|
184
|
+
equations : dict[str, dict] = {}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _get_function_from_name(name: str) -> callable:
|
|
188
|
+
for _type, funcs in equations.items():
|
|
189
|
+
if name in funcs:
|
|
190
|
+
return funcs[name]
|
|
191
|
+
raise ValueError(f"Equation `{name}` not implemented.")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def equation(type: str):
|
|
195
|
+
"""
|
|
196
|
+
Register an equation in the `equations` mapping under the specified type.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
type : str
|
|
201
|
+
The type of equation to register.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
decorator
|
|
206
|
+
The decorator function.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def decorator(func: callable):
|
|
210
|
+
if type not in equations:
|
|
211
|
+
equations[type] = {}
|
|
212
|
+
|
|
213
|
+
equations[type].update({func.__name__: func})
|
|
214
|
+
return func
|
|
215
|
+
return decorator
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@equation('discrete')
|
|
219
|
+
def discrete(x, rules: dict[str|int, int|float]) -> float:
|
|
220
|
+
return np.vectorize(rules.get, otypes=[np.float32])(x, np.nan)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@equation('sigmoid')
|
|
224
|
+
def logistic(x, a, b):
|
|
225
|
+
return 1 / (1 + np.exp(-a*(x - b)))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@equation('sigmoid')
|
|
229
|
+
def sigmoid(x):
|
|
230
|
+
return logistic(x, 1, 0)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@equation('sigmoid')
|
|
234
|
+
def vetharaniam2022_eq3(x, a, b):
|
|
235
|
+
return np.exp(a * (x - b)) / (1 + np.exp(a * (x - b)))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@equation('sigmoid')
|
|
239
|
+
def vetharaniam2022_eq5(x, a, b):
|
|
240
|
+
return 1 / (1 + np.exp(a * (np.sqrt(x) - np.sqrt(b))))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@equation('gaussian')
|
|
244
|
+
def vetharaniam2024_eq8(x, a, b, c):
|
|
245
|
+
return np.exp(-a * np.power(x - b, c))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@equation('gaussian')
|
|
249
|
+
def vetharaniam2024_eq10(x, a, b, c):
|
|
250
|
+
return 2 / (1 + np.exp(a * np.power(np.power(x, c) - np.power(b, c), 2)))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _rms_error(y_true, y_pred):
|
|
254
|
+
diff = abs(y_true - y_pred)
|
|
255
|
+
return np.sqrt(np.mean(diff**2))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _get_best_fit(methods, rmse, params, verbose=True):
|
|
259
|
+
best_fit = np.nanargmin(rmse)
|
|
260
|
+
if verbose:
|
|
261
|
+
print(f"""
|
|
262
|
+
Best fit: {methods[best_fit]}
|
|
263
|
+
RMSE: {rmse[best_fit]:.5f}
|
|
264
|
+
Params: a={params[best_fit][0]}, b={params[best_fit][1]}
|
|
265
|
+
""")
|
|
266
|
+
return methods[best_fit], params[best_fit]
|
lsapy/lsa.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Land Suitability definition."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Any, Union
|
|
4
|
+
import xarray as xr
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import geopandas as gpd
|
|
8
|
+
from shapely.geometry import mapping
|
|
9
|
+
import rioxarray
|
|
10
|
+
import regionmask
|
|
11
|
+
|
|
12
|
+
from lsapy.criteria import SuitabilityCriteria
|
|
13
|
+
from lsapy.statistics import statistics_summary, spatial_statistics_summary
|
|
14
|
+
|
|
15
|
+
__all__ = ["LandSuitability"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LandSuitability:
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
criteria: dict[str, SuitabilityCriteria],
|
|
24
|
+
short_name: Optional[str] = None,
|
|
25
|
+
long_name: Optional[str] = None,
|
|
26
|
+
description: Optional[str] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.name = name
|
|
29
|
+
self.criteria = criteria
|
|
30
|
+
self.short_name = short_name
|
|
31
|
+
self.long_name = long_name
|
|
32
|
+
self.description = description
|
|
33
|
+
|
|
34
|
+
self._sort_criteria_by_weight() # important if suitability as limited factor: return most weighted criteria if equals
|
|
35
|
+
self._criteria_name_list = [sc.name for sc in self.criteria.values()]
|
|
36
|
+
self._category_list = list(set([sc.category for sc in self.criteria.values()]))
|
|
37
|
+
self._get_params_by_category()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
if hasattr(self, 'data'):
|
|
42
|
+
return self.data.__repr__()
|
|
43
|
+
else:
|
|
44
|
+
attrs = []
|
|
45
|
+
for k, v in self.attrs.items():
|
|
46
|
+
if isinstance(v, str):
|
|
47
|
+
v = f"'{v}'"
|
|
48
|
+
attrs.append(f"{k}={v}")
|
|
49
|
+
return f"{self.__class__.__name__}({', '.join(attrs) if attrs else ''})"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def attrs(self):
|
|
54
|
+
return {k: v for k, v in {
|
|
55
|
+
'name': self.name,
|
|
56
|
+
'criteria': self._criteria_name_list,
|
|
57
|
+
'short_name': self.short_name,
|
|
58
|
+
'long_name': self.long_name,
|
|
59
|
+
'description': self.description,
|
|
60
|
+
}.items() if v is not None}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# def __getitem__(self, key: str) -> SuitabilityCriteria:
|
|
64
|
+
# return self.criteria[key]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compute_criteria_suitability(self, inplace: Optional[bool] = False) -> None | xr.Dataset:
|
|
68
|
+
sc_list = []
|
|
69
|
+
for _, sc in self.criteria.items():
|
|
70
|
+
print(f'Computing {sc.name}...')
|
|
71
|
+
if sc.indicator.attrs.get('compute', '') == 'done':
|
|
72
|
+
sc_list.append(sc.indicator.rename(sc.name))
|
|
73
|
+
else:
|
|
74
|
+
sc_list.append(sc.compute())
|
|
75
|
+
ls = xr.merge(sc_list, compat='override', combine_attrs='drop')
|
|
76
|
+
for sc in sc_list:
|
|
77
|
+
ls[sc.name].attrs = sc.attrs
|
|
78
|
+
ls.attrs = self.attrs
|
|
79
|
+
if inplace:
|
|
80
|
+
self.data = ls
|
|
81
|
+
else:
|
|
82
|
+
return ls
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def compute_category_suitability(
|
|
86
|
+
self, method: str,
|
|
87
|
+
keep_criteria: Optional[bool] = False,
|
|
88
|
+
inplace: Optional[bool] = False,
|
|
89
|
+
limit_var: Optional[bool] = False,
|
|
90
|
+
) -> xr.Dataset:
|
|
91
|
+
if not hasattr(self, 'data'):
|
|
92
|
+
ds = self.compute_criteria_suitability()
|
|
93
|
+
else:
|
|
94
|
+
ds = self.data
|
|
95
|
+
|
|
96
|
+
out = []
|
|
97
|
+
out_attrs = []
|
|
98
|
+
for category in self._category_list:
|
|
99
|
+
print(f'Computing {category}...')
|
|
100
|
+
sc_list = [sc for sc in self.criteria.values() if sc.category == category]
|
|
101
|
+
res = _aggregate_vars(ds[self._criteria_by_category[category]], method=method,
|
|
102
|
+
weights=[sc.weight for sc in sc_list], limit_var=limit_var)
|
|
103
|
+
if isinstance(res, xr.Dataset):
|
|
104
|
+
res = res.rename({'limiting_factor': f'{category}', 'limiting_var': f'{category}_var'})
|
|
105
|
+
else:
|
|
106
|
+
res = res.rename(f'{category}_suitability')
|
|
107
|
+
res.attrs.update({'long_name': f'{category.capitalize()} Suitability'})
|
|
108
|
+
out.append(res)
|
|
109
|
+
out_attrs.append(res.attrs)
|
|
110
|
+
out = xr.merge(out, compat='override', combine_attrs='drop')
|
|
111
|
+
if keep_criteria:
|
|
112
|
+
out = xr.merge([ds, out], compat='override', combine_attrs='drop')
|
|
113
|
+
for sc in ds.data_vars: # add attributes of criteria
|
|
114
|
+
out[sc].attrs = ds[sc].attrs
|
|
115
|
+
for i, category in enumerate(self._category_list): # add attributes of category suitability
|
|
116
|
+
out[f'{category}_suitability'].attrs = out_attrs[i]
|
|
117
|
+
|
|
118
|
+
out.attrs.update(self.attrs)
|
|
119
|
+
if inplace:
|
|
120
|
+
self.data = out
|
|
121
|
+
else:
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def compute_suitability(self,
|
|
126
|
+
method: Union[str, dict[str,str]] = 'mean',
|
|
127
|
+
by_category: Optional[bool] = False,
|
|
128
|
+
keep_all: Optional[bool] = False,
|
|
129
|
+
inplace: Optional[bool] = False,
|
|
130
|
+
limit_var: Optional[bool] = False
|
|
131
|
+
) -> xr.Dataset:
|
|
132
|
+
if isinstance(method, str):
|
|
133
|
+
cat_method, suit_method = method, method
|
|
134
|
+
elif isinstance(method, dict):
|
|
135
|
+
cat_method = method.get('category', 'mean')
|
|
136
|
+
suit_method = method.get('overall', 'mean')
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError("Method must be a string or a dictionary.")
|
|
139
|
+
|
|
140
|
+
if not hasattr(self, 'data'):
|
|
141
|
+
if by_category:
|
|
142
|
+
ds = self.compute_category_suitability(method=cat_method, keep_criteria=True, inplace=False, limit_var=limit_var)
|
|
143
|
+
else:
|
|
144
|
+
ds = self.compute_criteria_suitability(inplace=False)
|
|
145
|
+
else:
|
|
146
|
+
ds = self.data
|
|
147
|
+
|
|
148
|
+
if by_category:
|
|
149
|
+
weights = [self.weights_by_category[category] for category in self._category_list]
|
|
150
|
+
on_vars = [f'{category}_suitability' for category in self._category_list]
|
|
151
|
+
else :
|
|
152
|
+
weights = [sc.weight for sc in self.criteria.values()]
|
|
153
|
+
on_vars = self._criteria_name_list
|
|
154
|
+
|
|
155
|
+
print(f'Computing suitability...')
|
|
156
|
+
out = _aggregate_vars(ds[on_vars], method=suit_method, weights=weights, limit_var=limit_var).rename('suitability')
|
|
157
|
+
out.attrs.update({'long_name': 'Suitability'})
|
|
158
|
+
out_attrs = out.attrs
|
|
159
|
+
|
|
160
|
+
if keep_all:
|
|
161
|
+
out = xr.merge([ds, out], compat='override', combine_attrs='drop')
|
|
162
|
+
for sc in ds.data_vars: # add attributes of criteria
|
|
163
|
+
out[sc].attrs = ds[sc].attrs
|
|
164
|
+
out['suitability'].attrs = out_attrs
|
|
165
|
+
|
|
166
|
+
out.attrs.update(self.attrs)
|
|
167
|
+
if inplace:
|
|
168
|
+
self.data = out
|
|
169
|
+
else:
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _sort_criteria_by_weight(self) -> dict[str, SuitabilityCriteria]:
|
|
174
|
+
self.criteria = dict(sorted(self.criteria.items(), key=lambda item: item[1].weight, reverse=True))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_params_by_category(self):
|
|
178
|
+
self._get_criteria_by_category()
|
|
179
|
+
self._get_weights_by_category()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _get_criteria_by_category(self) -> dict[str, list[str]]:
|
|
183
|
+
self._criteria_by_category = {category: [] for category in self._category_list}
|
|
184
|
+
for sc in self.criteria.values():
|
|
185
|
+
self._criteria_by_category[sc.category].append(sc.name)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _get_weights_by_category(self) -> dict[str, float | int]:
|
|
189
|
+
self.weights_by_category = {category: [] for category in self._category_list}
|
|
190
|
+
for category in self._category_list:
|
|
191
|
+
self.weights_by_category[category] = sum([sc.weight for sc in self.criteria.values() if sc.category == category])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def mask(self, mask : Union[xr.DataArray, gpd.GeoDataFrame], inplace : Optional[bool] = False,
|
|
195
|
+
spatial_dims : Optional[tuple[str, str]] = None, crs : Optional[str] = None, invert : bool = False, **kwargs) -> xr.DataArray | xr.Dataset:
|
|
196
|
+
|
|
197
|
+
if not hasattr(self, 'data'):
|
|
198
|
+
raise ValueError("Suitability must be computed first.")
|
|
199
|
+
|
|
200
|
+
if inplace:
|
|
201
|
+
self.data = _mask_data(self.data, mask, spatial_dims=spatial_dims, crs=crs, invert=invert, **kwargs)
|
|
202
|
+
else:
|
|
203
|
+
return _mask_data(self.data, mask, spatial_dims=spatial_dims, crs=crs, invert=invert, **kwargs)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def statistics(
|
|
207
|
+
self,
|
|
208
|
+
on_vars: Optional[list] = None,
|
|
209
|
+
on_dims: Optional[list] = None,
|
|
210
|
+
on_dim_values: Optional[dict[str, Any]] = None,
|
|
211
|
+
bins : Optional[Union[list, np.ndarray]] = None,
|
|
212
|
+
bins_labels : Optional[list] = None,
|
|
213
|
+
all_bins : Optional[bool] = False,
|
|
214
|
+
cell_area : Optional[tuple[Union[float, str], str]] = None,
|
|
215
|
+
dropna: Optional[bool] = False,
|
|
216
|
+
**kwargs) -> pd.DataFrame:
|
|
217
|
+
|
|
218
|
+
if not hasattr(self, 'data'):
|
|
219
|
+
raise ValueError("Suitability must be computed first.")
|
|
220
|
+
|
|
221
|
+
return statistics_summary(self.data, on_vars=on_vars, on_dims=on_dims, on_dim_values=on_dim_values,
|
|
222
|
+
bins=bins, bins_labels=bins_labels, all_bins=all_bins,
|
|
223
|
+
cell_area=cell_area, dropna=dropna **kwargs)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def spatial_statistics(
|
|
227
|
+
self,
|
|
228
|
+
areas : gpd.GeoDataFrame,
|
|
229
|
+
name : Optional[str] = 'area',
|
|
230
|
+
on_vars: Optional[list] = None,
|
|
231
|
+
on_dims: Optional[list] = None,
|
|
232
|
+
on_dim_values: Optional[dict[str, Any]] = None,
|
|
233
|
+
bins : Optional[np.ndarray] = None,
|
|
234
|
+
all_bins : Optional[bool] = False,
|
|
235
|
+
cell_area : Optional[tuple[Union[float, str], str]] = None,
|
|
236
|
+
mask_kwargs : dict = {},
|
|
237
|
+
stats_kwargs: dict = {}) -> pd.DataFrame:
|
|
238
|
+
|
|
239
|
+
if not hasattr(self, 'data'):
|
|
240
|
+
raise ValueError("Suitability must be computed first.")
|
|
241
|
+
|
|
242
|
+
return spatial_statistics_summary(self.data, areas, name=name, on_vars=on_vars, on_dims=on_dims, on_dim_values=on_dim_values,
|
|
243
|
+
bins=bins, all_bins=all_bins, cell_area=cell_area, mask_kwargs=mask_kwargs, stats_kwargs=stats_kwargs)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _mask_data(data : xr.DataArray | xr.Dataset, mask : Union[xr.DataArray, gpd.GeoDataFrame],
|
|
247
|
+
spatial_dims : Optional[tuple[str, str]] = None,
|
|
248
|
+
crs : Optional[str] = None, invert : bool = False, **kwargs) -> xr.DataArray | xr.Dataset:
|
|
249
|
+
|
|
250
|
+
if isinstance(mask, gpd.GeoDataFrame):
|
|
251
|
+
mask = mask.to_crs(crs)
|
|
252
|
+
data = data.rio.set_spatial_dims(*spatial_dims).rio.write_crs(crs)
|
|
253
|
+
return data.rio.clip(mask.geometry.apply(mapping), invert=invert, **kwargs)
|
|
254
|
+
elif isinstance(mask, xr.DataArray):
|
|
255
|
+
if invert:
|
|
256
|
+
mask = ~mask
|
|
257
|
+
return data.where(mask, **kwargs)
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError('mask must be a GeoDataFrame or DataArray')
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
####################################################################################################
|
|
263
|
+
# VARIABLES AGGREGATION FUNCTIONS
|
|
264
|
+
####################################################################################################
|
|
265
|
+
|
|
266
|
+
def vars_weighted_mean(ds: xr.Dataset, vars = None, weights = None) -> xr.DataArray:
|
|
267
|
+
if vars is None:
|
|
268
|
+
vars = list(ds.data_vars)
|
|
269
|
+
if weights is None:
|
|
270
|
+
weights = np.ones(len(vars))
|
|
271
|
+
|
|
272
|
+
s = sum([ds[v] * w for v, w in zip(vars, weights)])
|
|
273
|
+
da: xr.DataArray = s / sum(weights)
|
|
274
|
+
return da.assign_attrs({
|
|
275
|
+
'method': 'Weighted Mean',
|
|
276
|
+
'description': f"Weighted Mean of variables: {', '.join([f'{v} ({w})' for v, w in zip(vars, weights)])}."
|
|
277
|
+
}).rename('weighted_mean')
|
|
278
|
+
|
|
279
|
+
def vars_mean(ds: xr.Dataset, vars = None) -> xr.DataArray:
|
|
280
|
+
if vars is None: vars = list(ds.data_vars)
|
|
281
|
+
da = vars_weighted_mean(ds, vars=vars)
|
|
282
|
+
return da.assign_attrs({'method': 'Mean', 'description': f"Mean of variables: {', '.join(vars)}."}).rename('mean')
|
|
283
|
+
|
|
284
|
+
def vars_weighted_geomean(ds: xr.Dataset, vars = None, weights = None) -> xr.DataArray:
|
|
285
|
+
if vars is None:
|
|
286
|
+
vars = list(ds.data_vars)
|
|
287
|
+
if weights is None:
|
|
288
|
+
weights = np.ones(len(vars))
|
|
289
|
+
|
|
290
|
+
s = sum([np.log(ds[v]) * w for v, w in zip(vars, weights)])
|
|
291
|
+
da : xr.DataArray = np.exp(s / sum(weights))
|
|
292
|
+
return da.assign_attrs({
|
|
293
|
+
'method': 'Weighted Geometric Mean',
|
|
294
|
+
'description': f"Weighted Geometric Mean of variables: {', '.join([f'{v} ({w})' for v, w in zip(vars, weights)])}."
|
|
295
|
+
}).rename('weighted_geometric_mean')
|
|
296
|
+
|
|
297
|
+
def vars_geomean(ds: xr.Dataset, vars = None) -> xr.DataArray:
|
|
298
|
+
if vars is None: vars = list(ds.data_vars)
|
|
299
|
+
da = vars_weighted_geomean(ds, vars=vars)
|
|
300
|
+
return da.assign_attrs({'method': 'Geometric Mean', 'description': f"Geometric Mean of variables: {', '.join(vars)}."}).rename('geometric_mean')
|
|
301
|
+
|
|
302
|
+
def limiting_factor(ds: xr.Dataset, vars = None, limiting_var: Optional[bool] = True) -> xr.DataArray | xr.Dataset:
|
|
303
|
+
if vars is None:
|
|
304
|
+
vars = list(ds.data_vars)
|
|
305
|
+
|
|
306
|
+
da = ds[vars].to_array()
|
|
307
|
+
mask = da.notnull().all(dim='variable')
|
|
308
|
+
|
|
309
|
+
lim = da.min(dim='variable', skipna=True).where(mask).rename('limiting_factor')
|
|
310
|
+
lim = lim.assign_attrs({'method': 'Limiting Factor', 'description': f"Value of limiting factor among variables: {', '.join(vars)}."})
|
|
311
|
+
if limiting_var:
|
|
312
|
+
lim_var = da.fillna(9999).argmin(dim='variable', skipna=True).where(mask).rename('limiting_var')
|
|
313
|
+
lim_var.attrs = {'method': 'Limiting Factor',
|
|
314
|
+
'description': f"Limiting factor among: {', '.join(vars)}.",
|
|
315
|
+
'legend': {f'{i}': v for i, v in enumerate(vars)}}
|
|
316
|
+
return xr.merge([lim, lim_var])
|
|
317
|
+
return lim.to_dataset()
|
|
318
|
+
|
|
319
|
+
def _aggregate_vars(ds : xr.Dataset, method: str = 'mean', vars = None, weights = None, **kwargs) -> xr.DataArray | xr.Dataset:
|
|
320
|
+
if method.lower() == 'mean':
|
|
321
|
+
return vars_mean(ds, vars=vars)
|
|
322
|
+
elif method.lower() == 'weighted_mean':
|
|
323
|
+
return vars_weighted_mean(ds, vars=vars, weights=weights)
|
|
324
|
+
elif method.lower() == 'geomean':
|
|
325
|
+
return vars_geomean(ds, vars=vars)
|
|
326
|
+
elif method.lower() == 'weighted_geomean':
|
|
327
|
+
return vars_weighted_geomean(ds, vars=vars, weights=weights)
|
|
328
|
+
elif method.lower() == 'limit_factor':
|
|
329
|
+
return limiting_factor(ds, vars=vars, **kwargs)
|
|
330
|
+
else:
|
|
331
|
+
raise ValueError(f"Method '{method}' not recognized.")
|
lsapy/statistics.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import Any, Optional, Union
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import xarray as xr
|
|
6
|
+
import regionmask
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def statistics_summary(
|
|
10
|
+
data: xr.DataArray | xr.Dataset,
|
|
11
|
+
on_vars: Optional[list] = None,
|
|
12
|
+
on_dims: Optional[list] = None,
|
|
13
|
+
on_dim_values: Optional[dict[str, Any]] = None,
|
|
14
|
+
bins : Optional[Union[list, np.ndarray]] = None,
|
|
15
|
+
bins_labels : Optional[list] = None,
|
|
16
|
+
all_bins : Optional[bool] = False,
|
|
17
|
+
cell_area : Optional[tuple[Union[float, str], str]] = None,
|
|
18
|
+
dropna: Optional[bool] = False,
|
|
19
|
+
**kwargs) -> pd.DataFrame:
|
|
20
|
+
|
|
21
|
+
def _correct_lowest_cut_interal(x: pd.Series) -> pd.Series:
|
|
22
|
+
first_cat = x.cat.categories[0]
|
|
23
|
+
lf = first_cat.left + (abs(first_cat.left) - bins[0])
|
|
24
|
+
return x.cat.rename_categories({first_cat: pd.Interval(lf, first_cat.right, closed='both')})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if bins is not None and bins_labels is not None and (len(bins)-1 != len(bins_labels)):
|
|
28
|
+
raise ValueError('bins and bins_labels must have the same length')
|
|
29
|
+
|
|
30
|
+
if on_dim_values is not None:
|
|
31
|
+
for dim, value in on_dim_values.items():
|
|
32
|
+
data = data.sel({dim: value})
|
|
33
|
+
|
|
34
|
+
if on_vars is None:
|
|
35
|
+
on_vars = list(data.data_vars)
|
|
36
|
+
data = data[on_vars]
|
|
37
|
+
if on_dims is None:
|
|
38
|
+
on_dims = list(data.dims)
|
|
39
|
+
on_dims = [d for d in on_dims if d not in ['lat', 'lon', 'x', 'y']] # remove spatial dims
|
|
40
|
+
if cell_area:
|
|
41
|
+
cell_area, cell_unit = cell_area
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
df = data.to_dataframe().reset_index()
|
|
45
|
+
if len(on_dims) > 0:
|
|
46
|
+
df = df.drop(columns=[c for c in data.coords if c not in on_dims])
|
|
47
|
+
|
|
48
|
+
df = df.melt(id_vars=on_dims)
|
|
49
|
+
_dims = ['variable'] + on_dims
|
|
50
|
+
|
|
51
|
+
if bins is not None:
|
|
52
|
+
df['bin'] = pd.cut(df['value'], bins=pd.Index(bins), **kwargs)
|
|
53
|
+
if 'include_lowest' in kwargs and kwargs['include_lowest'] == True:
|
|
54
|
+
df['bin'] = _correct_lowest_cut_interal(df['bin'])
|
|
55
|
+
|
|
56
|
+
if bins_labels is not None:
|
|
57
|
+
lab_mapping = dict(zip(df['bin'].cat.categories.astype(str), bins_labels))
|
|
58
|
+
_dims.append('bin')
|
|
59
|
+
if all_bins:
|
|
60
|
+
all_bins_inter = pd.Interval(df['bin'].cat.categories[0].left, df['bin'].cat.categories[-1].right, closed='both')
|
|
61
|
+
df_ = df.drop(columns=['bin']).assign(bin=all_bins_inter)
|
|
62
|
+
df_.loc[df['value'].isnull(), 'bin'] = np.nan
|
|
63
|
+
if bins_labels is not None:
|
|
64
|
+
lab_mapping.update({str(all_bins_inter) : 'all'})
|
|
65
|
+
df = pd.concat([df, df_])
|
|
66
|
+
df['bin'] = df['bin'].astype(str)
|
|
67
|
+
|
|
68
|
+
df_out = df.groupby(_dims, observed=False).describe().droplevel(0, axis=1).reset_index()
|
|
69
|
+
|
|
70
|
+
if bins_labels is not None:
|
|
71
|
+
bin_idx = np.where(df_out.columns == 'bin')[0][0]
|
|
72
|
+
df_out.insert(bin_idx + 1, 'bin_label', df_out['bin'].map(lab_mapping).values)
|
|
73
|
+
|
|
74
|
+
if cell_area:
|
|
75
|
+
df_out[f'area_{cell_unit}'] = df_out['count'] * cell_area
|
|
76
|
+
|
|
77
|
+
if dropna:
|
|
78
|
+
return df_out.dropna()
|
|
79
|
+
return df_out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def spatial_statistics_summary(
|
|
83
|
+
data: xr.DataArray | xr.Dataset,
|
|
84
|
+
areas : gpd.GeoDataFrame,
|
|
85
|
+
name : Optional[str] = 'area',
|
|
86
|
+
on_vars: Optional[list] = None,
|
|
87
|
+
on_dims: Optional[list] = None,
|
|
88
|
+
on_dim_values: Optional[dict[str, Any]] = None,
|
|
89
|
+
bins : Optional[np.ndarray] = None,
|
|
90
|
+
bins_labels : Optional[list] = None,
|
|
91
|
+
all_bins : Optional[bool] = False,
|
|
92
|
+
cell_area : Optional[tuple[Union[float, str], str]] = None,
|
|
93
|
+
dropna: Optional[bool] = False,
|
|
94
|
+
mask_kwargs : dict = {},
|
|
95
|
+
stats_kwargs: dict = {}) -> pd.DataFrame:
|
|
96
|
+
|
|
97
|
+
regions = regionmask.from_geopandas(areas, name=name, **mask_kwargs)
|
|
98
|
+
mask = regions.mask_3D(data)
|
|
99
|
+
|
|
100
|
+
out = []
|
|
101
|
+
for r in mask.region.values:
|
|
102
|
+
df = statistics_summary(
|
|
103
|
+
data.where(mask.sel(region=r)), on_vars=on_vars, on_dims=on_dims, on_dim_values=on_dim_values,
|
|
104
|
+
bins=bins, bins_labels=bins_labels, all_bins=all_bins,
|
|
105
|
+
cell_area=cell_area ,dropna=dropna, **stats_kwargs
|
|
106
|
+
)
|
|
107
|
+
df.insert(0, name, regions[r].name)
|
|
108
|
+
out.append(df)
|
|
109
|
+
return pd.concat(out)
|
lsapy/utils.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Miscellaneous Utilities
|
|
3
|
+
===============================
|
|
4
|
+
|
|
5
|
+
...
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import xarray as xr
|
|
11
|
+
|
|
12
|
+
def _load_data(name: str) -> xr.Dataset:
|
|
13
|
+
"""Load a sample dataset for testing or example purposes."""
|
|
14
|
+
if name.lower() not in ['soil', 'climate']:
|
|
15
|
+
raise ValueError(f"Invalid data name: {name}. Must be one of 'soil' or 'climate'.")
|
|
16
|
+
|
|
17
|
+
data_path = Path(__file__).parent / 'data'
|
|
18
|
+
return xr.open_dataset(data_path / f'{name.lower()}_sample.nc')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_soil_data() -> xr.Dataset:
|
|
22
|
+
"""Load soil dataset for testing or example purposes."""
|
|
23
|
+
return _load_data('soil')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_climate_data() -> xr.Dataset:
|
|
27
|
+
"""Load climate dataset for testing or example purposes."""
|
|
28
|
+
return _load_data('climate')
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lsapy
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: A Python package for Land Suitability Analysis.
|
|
5
|
+
Keywords: LSAPy,Land Suitability Analysis,Spatial Analysis,GIS,Geography
|
|
6
|
+
Author-email: Baptiste Hamon <baptiste.hamon@pg.canterbury.ac.nz>
|
|
7
|
+
Maintainer-email: Baptiste Hamon <baptiste.hamon@pg.canterbury.ac.nz>
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Project-URL: Homepage, https://github.com/baptistehamon/lsapy
|
|
19
|
+
Project-URL: Issues, https://github.com/baptistehamon/lsapy/issues
|
|
20
|
+
|
|
21
|
+
# LSAPy: Land Suitability Analysis for Python
|
|
22
|
+
|
|
23
|
+
`lsapy` stands for Land Suitability Analysis (LSA) for Python. The package has been designed to perfom spatial and temporal LSA operating on [xarray](https://docs.xarray.dev/en/stable/) `DataArray` and `Dataset`.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
- TODO
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
lsapy/__init__.py,sha256=MXu8EIXhOrPg7Aba-9nrQCU51QTRAWNKxDapzYcnzFs,284
|
|
2
|
+
lsapy/criteria.py,sha256=f2piZaEecKsLBvIH4JhLNXDNfNKNXUp70r23oeHErcQ,2971
|
|
3
|
+
lsapy/functions.py,sha256=WYfpUREOsrkv-70wme_zL_HArM_AI2b5YxU8vtee5NI,8382
|
|
4
|
+
lsapy/lsa.py,sha256=sBx96OpTDeaSrcX9s8IB2k91CNl0WUIW9hVM6oP-Hqg,14339
|
|
5
|
+
lsapy/statistics.py,sha256=62O57pqF1Pth4s1D0MbQEyobnxDGZ2bNatC2uVFnHH8,4269
|
|
6
|
+
lsapy/utils.py,sha256=eezLr5l0gqqgFEglzsSnbwx7JqOlVeKK3G63qllAJrg,776
|
|
7
|
+
lsapy/data/climate_sample.nc,sha256=ng2o7BxKLgzQWIl5fdiGWvdCQSeHeOO5-X3ciyD72Xo,52687793
|
|
8
|
+
lsapy/data/soil_sample.nc,sha256=y7NpYV9VZKXwiYExynA0vF1KAn5rhsWqzwGJ4tFE-TQ,2535031
|
|
9
|
+
lsapy-0.1.0.dev0.dist-info/licenses/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
|
10
|
+
lsapy-0.1.0.dev0.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
|
|
11
|
+
lsapy-0.1.0.dev0.dist-info/METADATA,sha256=Vr0peYa--97JnnQKHtAsNb8SdreyYCs7espEr4XSemg,1145
|
|
12
|
+
lsapy-0.1.0.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|