lsapy 0.1.0.dev0__py3-none-any.whl → 0.1.0.dev1__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 CHANGED
@@ -1,9 +1,9 @@
1
1
  """A Python package for Land Suitability Analysis."""
2
2
 
3
- from lsapy.lsa import LandSuitability
4
3
  from lsapy.criteria import SuitabilityCriteria
5
4
  from lsapy.functions import *
5
+ from lsapy.lsa import LandSuitability
6
6
 
7
7
  __author__ = "Baptiste Hamon"
8
8
  __email__ = "baptiste.hamon@pg.canterbury.ac.nz"
9
- __version__ = "0.1.0-dev0"
9
+ __version__ = "0.1.0-dev1"
lsapy/criteria.py CHANGED
@@ -1,22 +1,22 @@
1
1
  """Suitability Criteria definition."""
2
2
 
3
- from typing import Optional, Union, Any
4
3
  import xarray as xr
5
4
 
6
- from lsapy.functions import SuitabilityFunction, MembershipSuitFunction, DiscreteSuitFunction
5
+ from lsapy.functions import DiscreteSuitFunction, MembershipSuitFunction, SuitabilityFunction
7
6
 
8
7
  __all__ = ["SuitabilityCriteria"]
9
8
 
9
+
10
10
  class SuitabilityCriteria:
11
11
  def __init__(
12
12
  self,
13
13
  name: str,
14
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
15
+ func: SuitabilityFunction | MembershipSuitFunction | DiscreteSuitFunction,
16
+ weight: int | float | None = 1,
17
+ category: str | None = None,
18
+ long_name: str | None = None,
19
+ description: str | None = None
20
20
 
21
21
  ) -> None:
22
22
  self.name = name
@@ -27,7 +27,7 @@ class SuitabilityCriteria:
27
27
  self.long_name = long_name
28
28
  self.description = description
29
29
  self._from_indicator = _get_indicator_description(indicator)
30
-
30
+
31
31
  def __repr__(self) -> str:
32
32
  attrs = []
33
33
  attrs.append(f"name='{self.name}'")
@@ -41,17 +41,20 @@ class SuitabilityCriteria:
41
41
  if self.description is not None:
42
42
  attrs.append(f"description='{self.description}'")
43
43
  return f"{self.__class__.__name__}({', '.join(attrs) if attrs else ''})"
44
-
45
-
44
+
46
45
  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)
46
+ if self.func.func_method == 'discrete': # need to vectorize the discrete function
47
+ sc: xr.DataArray = xr.apply_ufunc(self.func.map, self.indicator).rename(self.name)
49
48
  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
-
49
+ sc: xr.DataArray = self.func.map(self.indicator).rename(self.name)
50
+ return sc.assign_attrs(
51
+ dict({k: v for k, v in self.attrs.items() if k not in ['name', 'func_method', 'from_indicator']},
52
+ **{
53
+ 'history': f"func_method: {self.func}; from_indicator: [{self._from_indicator}]",
54
+ 'compute': 'done'
55
+ })
56
+ )
57
+
55
58
  @property
56
59
  def attrs(self):
57
60
  return {k: v for k, v in {
@@ -69,4 +72,4 @@ def _get_indicator_description(indicator: xr.Dataset | xr.DataArray) -> str:
69
72
  if indicator.attrs != {}:
70
73
  return f"name: {indicator.name}; " + "; ".join([f"{k}: {v}" for k, v in indicator.attrs.items()])
71
74
  else:
72
- return f"name: {indicator.name}"
75
+ return f"name: {indicator.name}"
lsapy/functions.py CHANGED
@@ -1,13 +1,13 @@
1
1
  """Suitability Functions definitions."""
2
2
 
3
- from typing import Optional, Callable, Union, Any
4
3
  import warnings
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
5
7
  import matplotlib.pyplot as plt
6
8
  import numpy as np
7
- import xarray as xr
8
9
  from scipy.optimize import curve_fit
9
10
 
10
-
11
11
  __all__ = [
12
12
  "SuitabilityFunction",
13
13
  "MembershipSuitFunction",
@@ -19,42 +19,42 @@ class SuitabilityFunction:
19
19
 
20
20
  def __init__(
21
21
  self,
22
- func: Optional[Callable] = None,
23
- func_method: Optional[str] = None,
24
- func_params: Union[dict[str, Any]] = {}
22
+ func: Callable | None = None,
23
+ func_method: str | None = None,
24
+ func_params: dict[str, Any] = None
25
25
  ):
26
26
  if func_params is not None:
27
27
  if func is None and func_method is None:
28
28
  raise ValueError("If `func_params` is provided, `func` or `func_method` must also be provided.")
29
+ else:
30
+ func_params = {}
29
31
 
30
32
  self.func = func
31
33
  self.func_method = func_method
32
34
  self.func_params = func_params
33
35
  if func is None and func_method is not None:
34
36
  self.func = _get_function_from_name(func_method)
35
-
36
37
 
37
38
  def __repr__(self):
38
- return f"{self.__class__.__name__}(func={self.func.__name__}, func_method='{self.func_method}', func_params={self.func_params})"
39
-
39
+ return (f"{self.__class__.__name__}("
40
+ f"func={self.func.__name__}, "
41
+ f"func_method='{self.func_method}', "
42
+ f"func_params={self.func_params})")
40
43
 
41
44
  def __call__(self, x):
42
45
  if self.func is None:
43
46
  raise ValueError("No function has been provided.")
44
47
  return self.func(x, **self.func_params)
45
-
46
-
48
+
47
49
  def map(self, x):
48
50
  return self(x)
49
-
50
51
 
51
52
  def plot(self, x) -> None:
52
53
  plt.plot(x, self(x))
53
54
 
54
-
55
55
  @property
56
56
  def attrs(self):
57
- if self.func_method == None and self.func_params == None:
57
+ if self.func_method is None and self.func_params is None:
58
58
  return {}
59
59
  return {k: v for k, v in {
60
60
  'func_method': self.func_method,
@@ -71,16 +71,17 @@ class MembershipSuitFunction(SuitabilityFunction):
71
71
 
72
72
  def __init__(
73
73
  self,
74
- func: Optional[Callable] = None,
75
- func_method: Optional[str] = None,
76
- func_params: Optional[dict[str, int | float]] = None
74
+ func: Callable | None = None,
75
+ func_method: str | None = None,
76
+ func_params: dict[str, int | float] | None = None
77
77
  ):
78
78
  super().__init__(func, func_method, func_params)
79
79
 
80
-
81
80
  @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)
81
+ def fit(x, y=None, methods: str | list[str] = 'all', plot: bool = False):
82
+ if y is None:
83
+ y = [0, .25, .5, .75, 1]
84
+ return _fit_mbs_functions(x, np.array(y), methods, plot)
84
85
 
85
86
 
86
87
  def _prepare_for_fitting(methods: str | list[str] = 'all'):
@@ -92,16 +93,16 @@ def _prepare_for_fitting(methods: str | list[str] = 'all'):
92
93
  elif isinstance(methods, list) or isinstance(methods, str):
93
94
  if isinstance(methods, str):
94
95
  methods = [methods]
95
-
96
+
96
97
  _methods = []
97
98
  for method in methods:
98
99
  if method in _types:
99
100
  [_methods.append(m) for m in equations[method.replace('_like', '')].keys()]
100
101
  else:
101
- try :
102
+ try:
102
103
  _get_function_from_name(method)
103
104
  _methods.append(method)
104
- except :
105
+ except Exception:
105
106
  _skipped.append(method)
106
107
  warnings.warn(f"`{method}` not found in equations. Skipped.", stacklevel=2)
107
108
  methods = _methods
@@ -125,7 +126,7 @@ def _get_function_p0(method: str, x: np.ndarray) -> list[float]:
125
126
 
126
127
 
127
128
  def _fit_mbs_functions(x, y, methods: str | list[str] = 'all', plot: bool = False):
128
-
129
+
129
130
  skipped = []
130
131
  methods, _skipped = _prepare_for_fitting(methods)
131
132
  skipped.extend(_skipped)
@@ -133,22 +134,22 @@ def _fit_mbs_functions(x, y, methods: str | list[str] = 'all', plot: bool = Fals
133
134
  if len(methods) == 0:
134
135
  print(f"Skipped fitting for the following methods: {', '.join(skipped)}.")
135
136
  raise ValueError("No methods to fit.")
136
- else :
137
+ else:
137
138
  x_ = np.linspace(min(x), max(x), 100)
138
139
  rms_errors = []
139
140
  f_params = []
140
141
  for method in methods:
141
- try :
142
+ try:
142
143
  f = _get_function_from_name(method)
143
144
  p0 = _get_function_p0(method, x)
144
145
  popt, _ = curve_fit(f, x, y, p0=p0, maxfev=15000)
145
146
  y_ = f(x_, *popt)
146
147
  f_params.append(popt)
147
- rmse = _rms_error(y,f(x, *popt))
148
+ rmse = _rms_error(y, f(x, *popt))
148
149
  rms_errors.append(rmse)
149
150
  if plot:
150
151
  plt.plot(x_, y_, label=method + f' (RMSE={rmse:.2f})')
151
- except:
152
+ except Exception:
152
153
  skipped.append(method)
153
154
  warnings.warn(f"Failed to fit `{method}`. Skipped.", stacklevel=2)
154
155
  if plot:
@@ -158,7 +159,7 @@ def _fit_mbs_functions(x, y, methods: str | list[str] = 'all', plot: bool = Fals
158
159
 
159
160
  if len(skipped) > 0:
160
161
  print(f"Skipped fitting for the following methods: {', '.join(skipped)}.")
161
- f_best, p_best = _get_best_fit(methods, rms_errors, f_params)
162
+ f_best, p_best = _get_best_fit([m for m in methods if m not in skipped], rms_errors, f_params)
162
163
  return _get_function_from_name(f_best), p_best
163
164
 
164
165
 
@@ -170,7 +171,7 @@ class DiscreteSuitFunction(SuitabilityFunction):
170
171
 
171
172
  def __init__(
172
173
  self,
173
- func_params: Optional[dict[str, int | float]] = None
174
+ func_params: dict[str, int | float] | None = None
174
175
  ):
175
176
  self.func = discrete
176
177
  self.func_method = 'discrete'
@@ -181,7 +182,7 @@ class DiscreteSuitFunction(SuitabilityFunction):
181
182
  # ---------------------------- Utility functions ----------------------------- #
182
183
  # ---------------------------------------------------------------------------- #
183
184
 
184
- equations : dict[str, dict] = {}
185
+ equations: dict[str, dict] = {}
185
186
 
186
187
 
187
188
  def _get_function_from_name(name: str) -> callable:
@@ -199,7 +200,7 @@ def equation(type: str):
199
200
  ----------
200
201
  type : str
201
202
  The type of equation to register.
202
-
203
+
203
204
  Returns
204
205
  -------
205
206
  decorator
@@ -216,13 +217,13 @@ def equation(type: str):
216
217
 
217
218
 
218
219
  @equation('discrete')
219
- def discrete(x, rules: dict[str|int, int|float]) -> float:
220
+ def discrete(x, rules: dict[str | int, int | float]) -> float:
220
221
  return np.vectorize(rules.get, otypes=[np.float32])(x, np.nan)
221
222
 
222
223
 
223
224
  @equation('sigmoid')
224
225
  def logistic(x, a, b):
225
- return 1 / (1 + np.exp(-a*(x - b)))
226
+ return 1 / (1 + np.exp(-a * (x - b)))
226
227
 
227
228
 
228
229
  @equation('sigmoid')
lsapy/lsa.py CHANGED
@@ -1,29 +1,27 @@
1
1
  """Land Suitability definition."""
2
2
 
3
- from typing import Optional, Any, Union
4
- import xarray as xr
3
+ from typing import Any
4
+
5
+ import geopandas as gpd
5
6
  import numpy as np
6
7
  import pandas as pd
7
- import geopandas as gpd
8
+ import xarray as xr
8
9
  from shapely.geometry import mapping
9
- import rioxarray
10
- import regionmask
11
10
 
12
11
  from lsapy.criteria import SuitabilityCriteria
13
- from lsapy.statistics import statistics_summary, spatial_statistics_summary
12
+ from lsapy.statistics import spatial_statistics_summary, statistics_summary
14
13
 
15
14
  __all__ = ["LandSuitability"]
16
15
 
17
16
 
18
17
  class LandSuitability:
19
-
20
18
  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,
19
+ self,
20
+ name: str,
21
+ criteria: dict[str, SuitabilityCriteria],
22
+ short_name: str | None = None,
23
+ long_name: str | None = None,
24
+ description: str | None = None,
27
25
  ) -> None:
28
26
  self.name = name
29
27
  self.criteria = criteria
@@ -31,48 +29,50 @@ class LandSuitability:
31
29
  self.long_name = long_name
32
30
  self.description = description
33
31
 
34
- self._sort_criteria_by_weight() # important if suitability as limited factor: return most weighted criteria if equals
32
+ self._sort_criteria_by_weight() # important if suitability as limited factor
35
33
  self._criteria_name_list = [sc.name for sc in self.criteria.values()]
36
34
  self._category_list = list(set([sc.category for sc in self.criteria.values()]))
37
35
  self._get_params_by_category()
38
36
 
39
-
40
37
  def __repr__(self) -> str:
41
- if hasattr(self, 'data'):
38
+ if hasattr(self, "data"):
42
39
  return self.data.__repr__()
43
40
  else:
44
41
  attrs = []
45
42
  for k, v in self.attrs.items():
46
43
  if isinstance(v, str):
47
- v = f"'{v}'"
48
- attrs.append(f"{k}={v}")
44
+ v_ = f"'{v}'"
45
+ else:
46
+ v_ = v
47
+ attrs.append(f"{k}={v_}")
49
48
  return f"{self.__class__.__name__}({', '.join(attrs) if attrs else ''})"
50
-
51
49
 
52
50
  @property
53
51
  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
-
52
+ return {
53
+ k: v
54
+ 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()
61
+ if v is not None
62
+ }
62
63
 
63
64
  # def __getitem__(self, key: str) -> SuitabilityCriteria:
64
65
  # return self.criteria[key]
65
66
 
66
-
67
- def compute_criteria_suitability(self, inplace: Optional[bool] = False) -> None | xr.Dataset:
67
+ def compute_criteria_suitability(self, inplace: bool | None = False) -> None | xr.Dataset:
68
68
  sc_list = []
69
69
  for _, sc in self.criteria.items():
70
- print(f'Computing {sc.name}...')
71
- if sc.indicator.attrs.get('compute', '') == 'done':
70
+ print(f"Computing {sc.name}...")
71
+ if sc.indicator.attrs.get("compute", "") == "done":
72
72
  sc_list.append(sc.indicator.rename(sc.name))
73
73
  else:
74
74
  sc_list.append(sc.compute())
75
- ls = xr.merge(sc_list, compat='override', combine_attrs='drop')
75
+ ls = xr.merge(sc_list, compat="override", combine_attrs="drop")
76
76
  for sc in sc_list:
77
77
  ls[sc.name].attrs = sc.attrs
78
78
  ls.attrs = self.attrs
@@ -80,173 +80,205 @@ class LandSuitability:
80
80
  self.data = ls
81
81
  else:
82
82
  return ls
83
-
84
83
 
85
84
  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,
85
+ self,
86
+ method: str,
87
+ keep_criteria: bool | None = False,
88
+ inplace: bool | None = False,
89
+ limit_var: bool | None = False,
90
90
  ) -> xr.Dataset:
91
- if not hasattr(self, 'data'):
91
+ if not hasattr(self, "data"):
92
92
  ds = self.compute_criteria_suitability()
93
93
  else:
94
94
  ds = self.data
95
-
95
+
96
96
  out = []
97
97
  out_attrs = []
98
98
  for category in self._category_list:
99
- print(f'Computing {category}...')
99
+ print(f"Computing {category}...")
100
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)
101
+ res = _aggregate_vars(
102
+ ds[self._criteria_by_category[category]],
103
+ method=method,
104
+ weights=[sc.weight for sc in sc_list],
105
+ limit_var=limit_var,
106
+ )
103
107
  if isinstance(res, xr.Dataset):
104
- res = res.rename({'limiting_factor': f'{category}', 'limiting_var': f'{category}_var'})
108
+ res = res.rename({"limiting_factor": f"{category}", "limiting_var": f"{category}_var"})
105
109
  else:
106
- res = res.rename(f'{category}_suitability')
107
- res.attrs.update({'long_name': f'{category.capitalize()} Suitability'})
110
+ res = res.rename(f"{category}_suitability")
111
+ res.attrs.update({"long_name": f"{category.capitalize()} Suitability"})
108
112
  out.append(res)
109
113
  out_attrs.append(res.attrs)
110
- out = xr.merge(out, compat='override', combine_attrs='drop')
114
+ out = xr.merge(out, compat="override", combine_attrs="drop")
111
115
  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
116
+ out = xr.merge([ds, out], compat="override", combine_attrs="drop")
117
+ for sc in ds.data_vars: # add attributes of criteria
114
118
  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
-
119
+ for i, category in enumerate(self._category_list): # add attributes of category suitability
120
+ out[f"{category}_suitability"].attrs = out_attrs[i]
121
+
118
122
  out.attrs.update(self.attrs)
119
123
  if inplace:
120
124
  self.data = out
121
125
  else:
122
126
  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
127
+
128
+ def compute_suitability(
129
+ self,
130
+ method: str | dict[str, str] = "mean",
131
+ by_category: bool | None = False,
132
+ keep_all: bool | None = False,
133
+ inplace: bool | None = False,
134
+ limit_var: bool | None = False,
131
135
  ) -> xr.Dataset:
132
136
  if isinstance(method, str):
133
137
  cat_method, suit_method = method, method
134
138
  elif isinstance(method, dict):
135
- cat_method = method.get('category', 'mean')
136
- suit_method = method.get('overall', 'mean')
139
+ cat_method = method.get("category", "mean")
140
+ suit_method = method.get("overall", "mean")
137
141
  else:
138
142
  raise ValueError("Method must be a string or a dictionary.")
139
-
140
- if not hasattr(self, 'data'):
143
+
144
+ if not hasattr(self, "data"):
141
145
  if by_category:
142
- ds = self.compute_category_suitability(method=cat_method, keep_criteria=True, inplace=False, limit_var=limit_var)
146
+ ds = self.compute_category_suitability(
147
+ method=cat_method, keep_criteria=True, inplace=False, limit_var=limit_var
148
+ )
143
149
  else:
144
150
  ds = self.compute_criteria_suitability(inplace=False)
145
151
  else:
146
152
  ds = self.data
147
-
153
+
148
154
  if by_category:
149
155
  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 :
156
+ on_vars = [f"{category}_suitability" for category in self._category_list]
157
+ else:
152
158
  weights = [sc.weight for sc in self.criteria.values()]
153
159
  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'})
160
+
161
+ print("Computing suitability...")
162
+ out = _aggregate_vars(ds[on_vars], method=suit_method, weights=weights, limit_var=limit_var).rename(
163
+ "suitability"
164
+ )
165
+ out.attrs.update({"long_name": "Suitability"})
158
166
  out_attrs = out.attrs
159
167
 
160
168
  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
169
+ out = xr.merge([ds, out], compat="override", combine_attrs="drop")
170
+ for sc in ds.data_vars: # add attributes of criteria
163
171
  out[sc].attrs = ds[sc].attrs
164
- out['suitability'].attrs = out_attrs
165
-
172
+ out["suitability"].attrs = out_attrs
173
+
166
174
  out.attrs.update(self.attrs)
167
175
  if inplace:
168
176
  self.data = out
169
177
  else:
170
178
  return out
171
-
172
179
 
173
180
  def _sort_criteria_by_weight(self) -> dict[str, SuitabilityCriteria]:
174
181
  self.criteria = dict(sorted(self.criteria.items(), key=lambda item: item[1].weight, reverse=True))
175
182
 
176
-
177
183
  def _get_params_by_category(self):
178
184
  self._get_criteria_by_category()
179
185
  self._get_weights_by_category()
180
-
181
-
186
+
182
187
  def _get_criteria_by_category(self) -> dict[str, list[str]]:
183
188
  self._criteria_by_category = {category: [] for category in self._category_list}
184
189
  for sc in self.criteria.values():
185
190
  self._criteria_by_category[sc.category].append(sc.name)
186
-
187
191
 
188
192
  def _get_weights_by_category(self) -> dict[str, float | int]:
189
193
  self.weights_by_category = {category: [] for category in self._category_list}
190
194
  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'):
195
+ self.weights_by_category[category] = sum(
196
+ [sc.weight for sc in self.criteria.values() if sc.category == category]
197
+ )
198
+
199
+ def mask(
200
+ self,
201
+ mask: xr.DataArray | gpd.GeoDataFrame,
202
+ inplace: bool | None = False,
203
+ spatial_dims: tuple[str, str] | None = None,
204
+ crs: str | None = None,
205
+ invert: bool = False,
206
+ **kwargs,
207
+ ) -> xr.DataArray | xr.Dataset:
208
+ if not hasattr(self, "data"):
198
209
  raise ValueError("Suitability must be computed first.")
199
-
210
+
200
211
  if inplace:
201
212
  self.data = _mask_data(self.data, mask, spatial_dims=spatial_dims, crs=crs, invert=invert, **kwargs)
202
- else:
213
+ else:
203
214
  return _mask_data(self.data, mask, spatial_dims=spatial_dims, crs=crs, invert=invert, **kwargs)
204
-
205
215
 
206
216
  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'):
217
+ self,
218
+ on_vars: list | None = None,
219
+ on_dims: list | None = None,
220
+ on_dim_values: dict[str, Any] | None = None,
221
+ bins: list | np.ndarray | None = None,
222
+ bins_labels: list | None = None,
223
+ all_bins: bool | None = False,
224
+ cell_area: tuple[float | str, str] | None = None,
225
+ dropna: bool | None = False,
226
+ **kwargs,
227
+ ) -> pd.DataFrame:
228
+ if not hasattr(self, "data"):
219
229
  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
-
230
+
231
+ return statistics_summary(
232
+ self.data,
233
+ on_vars=on_vars,
234
+ on_dims=on_dims,
235
+ on_dim_values=on_dim_values,
236
+ bins=bins,
237
+ bins_labels=bins_labels,
238
+ all_bins=all_bins,
239
+ cell_area=cell_area,
240
+ dropna=dropna**kwargs,
241
+ )
225
242
 
226
243
  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'):
244
+ self,
245
+ areas: gpd.GeoDataFrame,
246
+ name: str | None = "area",
247
+ on_vars: list | None = None,
248
+ on_dims: list | None = None,
249
+ on_dim_values: dict[str, Any] | None = None,
250
+ bins: np.ndarray | None = None,
251
+ all_bins: bool | None = False,
252
+ cell_area: tuple[float | str, str] | None = None,
253
+ mask_kwargs: dict = None,
254
+ stats_kwargs: dict = None,
255
+ ) -> pd.DataFrame:
256
+ if not hasattr(self, "data"):
240
257
  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
258
 
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
-
259
+ return spatial_statistics_summary(
260
+ self.data,
261
+ areas,
262
+ name=name,
263
+ on_vars=on_vars,
264
+ on_dims=on_dims,
265
+ on_dim_values=on_dim_values,
266
+ bins=bins,
267
+ all_bins=all_bins,
268
+ cell_area=cell_area,
269
+ mask_kwargs=mask_kwargs,
270
+ stats_kwargs=stats_kwargs,
271
+ )
272
+
273
+
274
+ def _mask_data(
275
+ data: xr.DataArray | xr.Dataset,
276
+ mask: xr.DataArray | gpd.GeoDataFrame,
277
+ spatial_dims: tuple[str, str] | None = None,
278
+ crs: str | None = None,
279
+ invert: bool = False,
280
+ **kwargs,
281
+ ) -> xr.DataArray | xr.Dataset:
250
282
  if isinstance(mask, gpd.GeoDataFrame):
251
283
  mask = mask.to_crs(crs)
252
284
  data = data.rio.set_spatial_dims(*spatial_dims).rio.write_crs(crs)
@@ -256,76 +288,102 @@ def _mask_data(data : xr.DataArray | xr.Dataset, mask : Union[xr.DataArray, gpd.
256
288
  mask = ~mask
257
289
  return data.where(mask, **kwargs)
258
290
  else:
259
- raise ValueError('mask must be a GeoDataFrame or DataArray')
291
+ raise ValueError("mask must be a GeoDataFrame or DataArray")
260
292
 
261
293
 
262
294
  ####################################################################################################
263
295
  # VARIABLES AGGREGATION FUNCTIONS
264
296
  ####################################################################################################
265
297
 
266
- def vars_weighted_mean(ds: xr.Dataset, vars = None, weights = None) -> xr.DataArray:
298
+
299
+ def vars_weighted_mean(ds: xr.Dataset, vars=None, weights=None) -> xr.DataArray:
267
300
  if vars is None:
268
301
  vars = list(ds.data_vars)
269
302
  if weights is None:
270
303
  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
304
 
279
- def vars_mean(ds: xr.Dataset, vars = None) -> xr.DataArray:
280
- if vars is None: vars = list(ds.data_vars)
305
+ s = sum([ds[v] * w for v, w in zip(vars, weights, strict=False)])
306
+ da: xr.DataArray = s / sum(weights)
307
+ return da.assign_attrs(
308
+ {
309
+ "method": "Weighted Mean",
310
+ "descritpion": (
311
+ "Weighted Mean of variables: "
312
+ f"{', '.join([f'{v} ({w})' for v, w in zip(vars, weights, strict=False)])}."
313
+ )
314
+ }
315
+ ).rename("weighted_mean")
316
+
317
+
318
+ def vars_mean(ds: xr.Dataset, vars=None) -> xr.DataArray:
319
+ if vars is None:
320
+ vars = list(ds.data_vars)
281
321
  da = vars_weighted_mean(ds, vars=vars)
282
- return da.assign_attrs({'method': 'Mean', 'description': f"Mean of variables: {', '.join(vars)}."}).rename('mean')
322
+ return da.assign_attrs({"method": "Mean", "description": f"Mean of variables: {', '.join(vars)}."}).rename("mean")
323
+
283
324
 
284
- def vars_weighted_geomean(ds: xr.Dataset, vars = None, weights = None) -> xr.DataArray:
325
+ def vars_weighted_geomean(ds: xr.Dataset, vars=None, weights=None) -> xr.DataArray:
285
326
  if vars is None:
286
327
  vars = list(ds.data_vars)
287
328
  if weights is None:
288
329
  weights = np.ones(len(vars))
289
330
 
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')
331
+ s = sum([np.log(ds[v]) * w for v, w in zip(vars, weights, strict=False)])
332
+ da: xr.DataArray = np.exp(s / sum(weights))
333
+ return da.assign_attrs(
334
+ {
335
+ "method": "Weighted Geometric Mean",
336
+ "description": (
337
+ "Weighted Geometric Mean of variables: "
338
+ f"{', '.join([f'{v} ({w})' for v, w in zip(vars, weights, strict=False)])}."
339
+ )
340
+ }
341
+ ).rename("weighted_geometric_mean")
296
342
 
297
- def vars_geomean(ds: xr.Dataset, vars = None) -> xr.DataArray:
298
- if vars is None: vars = list(ds.data_vars)
343
+
344
+ def vars_geomean(ds: xr.Dataset, vars=None) -> xr.DataArray:
345
+ if vars is None:
346
+ vars = list(ds.data_vars)
299
347
  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')
348
+ return da.assign_attrs(
349
+ {"method": "Geometric Mean", "description": f"Geometric Mean of variables: {', '.join(vars)}."}
350
+ ).rename("geometric_mean")
351
+
301
352
 
302
- def limiting_factor(ds: xr.Dataset, vars = None, limiting_var: Optional[bool] = True) -> xr.DataArray | xr.Dataset:
353
+ def limiting_factor(ds: xr.Dataset, vars=None, limiting_var: bool | None = True) -> xr.DataArray | xr.Dataset:
303
354
  if vars is None:
304
355
  vars = list(ds.data_vars)
305
-
356
+
306
357
  da = ds[vars].to_array()
307
- mask = da.notnull().all(dim='variable')
358
+ mask = da.notnull().all(dim="variable")
308
359
 
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)}."})
360
+ lim = da.min(dim="variable", skipna=True).where(mask).rename("limiting_factor")
361
+ lim = lim.assign_attrs(
362
+ {"method": "Limiting Factor", "description": f"Value of limiting factor among variables: {', '.join(vars)}."}
363
+ )
311
364
  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)}}
365
+ lim_var = da.fillna(9999).argmin(dim="variable", skipna=True).where(mask).rename("limiting_var")
366
+ lim_var.attrs = {
367
+ "method": "Limiting Factor",
368
+ "description": f"Limiting factor among: {', '.join(vars)}.",
369
+ "legend": {f"{i}": v for i, v in enumerate(vars)},
370
+ }
316
371
  return xr.merge([lim, lim_var])
317
372
  return lim.to_dataset()
318
373
 
319
- def _aggregate_vars(ds : xr.Dataset, method: str = 'mean', vars = None, weights = None, **kwargs) -> xr.DataArray | xr.Dataset:
320
- if method.lower() == 'mean':
374
+
375
+ def _aggregate_vars(
376
+ ds: xr.Dataset, method: str = "mean", vars=None, weights=None, **kwargs
377
+ ) -> xr.DataArray | xr.Dataset:
378
+ if method.lower() == "mean":
321
379
  return vars_mean(ds, vars=vars)
322
- elif method.lower() == 'weighted_mean':
380
+ elif method.lower() == "weighted_mean":
323
381
  return vars_weighted_mean(ds, vars=vars, weights=weights)
324
- elif method.lower() == 'geomean':
382
+ elif method.lower() == "geomean":
325
383
  return vars_geomean(ds, vars=vars)
326
- elif method.lower() == 'weighted_geomean':
384
+ elif method.lower() == "weighted_geomean":
327
385
  return vars_weighted_geomean(ds, vars=vars, weights=weights)
328
- elif method.lower() == 'limit_factor':
386
+ elif method.lower() == "limit_factor":
329
387
  return limiting_factor(ds, vars=vars, **kwargs)
330
388
  else:
331
389
  raise ValueError(f"Method '{method}' not recognized.")
lsapy/statistics.py CHANGED
@@ -1,67 +1,70 @@
1
- from typing import Any, Optional, Union
1
+ """Statistics Module"""
2
+
3
+ from typing import Any
4
+
5
+ import geopandas as gpd
2
6
  import numpy as np
3
7
  import pandas as pd
4
- import geopandas as gpd
5
- import xarray as xr
6
8
  import regionmask
9
+ import xarray as xr
7
10
 
8
11
 
9
12
  def statistics_summary(
10
13
  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,
14
+ on_vars: list | None = None,
15
+ on_dims: list | None = None,
16
+ on_dim_values: dict[str, Any] | None = None,
17
+ bins: list | np.ndarray | None = None,
18
+ bins_labels: list | None = None,
19
+ all_bins: bool | None = False,
20
+ cell_area: tuple[float | str, str] | None = None,
21
+ dropna: bool | None = False,
19
22
  **kwargs) -> pd.DataFrame:
20
-
23
+
21
24
  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)):
25
+ first_cat = x.cat.categories[0]
26
+ lf = first_cat.left + (abs(first_cat.left) - bins[0])
27
+ return x.cat.rename_categories({first_cat: pd.Interval(lf, first_cat.right, closed='both')})
28
+
29
+ if bins is not None and bins_labels is not None and (len(bins) - 1 != len(bins_labels)):
28
30
  raise ValueError('bins and bins_labels must have the same length')
29
-
31
+
30
32
  if on_dim_values is not None:
31
33
  for dim, value in on_dim_values.items():
32
34
  data = data.sel({dim: value})
33
-
35
+
34
36
  if on_vars is None:
35
37
  on_vars = list(data.data_vars)
36
38
  data = data[on_vars]
37
39
  if on_dims is None:
38
40
  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
41
+ on_dims = [d for d in on_dims if d not in ['lat', 'lon', 'x', 'y']] # remove spatial dims
40
42
  if cell_area:
41
43
  cell_area, cell_unit = cell_area
42
44
 
43
-
44
45
  df = data.to_dataframe().reset_index()
45
46
  if len(on_dims) > 0:
46
47
  df = df.drop(columns=[c for c in data.coords if c not in on_dims])
47
-
48
+
48
49
  df = df.melt(id_vars=on_dims)
49
50
  _dims = ['variable'] + on_dims
50
51
 
51
52
  if bins is not None:
52
53
  df['bin'] = pd.cut(df['value'], bins=pd.Index(bins), **kwargs)
53
- if 'include_lowest' in kwargs and kwargs['include_lowest'] == True:
54
+ if 'include_lowest' in kwargs and kwargs['include_lowest']:
54
55
  df['bin'] = _correct_lowest_cut_interal(df['bin'])
55
56
 
56
57
  if bins_labels is not None:
57
- lab_mapping = dict(zip(df['bin'].cat.categories.astype(str), bins_labels))
58
+ lab_mapping = dict(zip(df['bin'].cat.categories.astype(str), bins_labels, strict=False))
58
59
  _dims.append('bin')
59
60
  if all_bins:
60
- all_bins_inter = pd.Interval(df['bin'].cat.categories[0].left, df['bin'].cat.categories[-1].right, closed='both')
61
+ all_bins_inter = pd.Interval(
62
+ df['bin'].cat.categories[0].left, df['bin'].cat.categories[-1].right, closed='both'
63
+ )
61
64
  df_ = df.drop(columns=['bin']).assign(bin=all_bins_inter)
62
65
  df_.loc[df['value'].isnull(), 'bin'] = np.nan
63
66
  if bins_labels is not None:
64
- lab_mapping.update({str(all_bins_inter) : 'all'})
67
+ lab_mapping.update({str(all_bins_inter): 'all'})
65
68
  df = pd.concat([df, df_])
66
69
  df['bin'] = df['bin'].astype(str)
67
70
 
@@ -73,7 +76,7 @@ def statistics_summary(
73
76
 
74
77
  if cell_area:
75
78
  df_out[f'area_{cell_unit}'] = df_out['count'] * cell_area
76
-
79
+
77
80
  if dropna:
78
81
  return df_out.dropna()
79
82
  return df_out
@@ -81,19 +84,24 @@ def statistics_summary(
81
84
 
82
85
  def spatial_statistics_summary(
83
86
  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
-
87
+ areas: gpd.GeoDataFrame,
88
+ name: str | None = 'area',
89
+ on_vars: list | None = None,
90
+ on_dims: list | None = None,
91
+ on_dim_values: dict[str, Any] | None = None,
92
+ bins: np.ndarray | None = None,
93
+ bins_labels: list | None = None,
94
+ all_bins: bool | None = False,
95
+ cell_area: tuple[float | str, str] | None = None,
96
+ dropna: bool | None = False,
97
+ mask_kwargs: dict = None,
98
+ stats_kwargs: dict = None) -> pd.DataFrame:
99
+
100
+ if mask_kwargs is None:
101
+ mask_kwargs = {}
102
+ if stats_kwargs is None:
103
+ stats_kwargs = {}
104
+
97
105
  regions = regionmask.from_geopandas(areas, name=name, **mask_kwargs)
98
106
  mask = regions.mask_3D(data)
99
107
 
@@ -102,8 +110,8 @@ def spatial_statistics_summary(
102
110
  df = statistics_summary(
103
111
  data.where(mask.sel(region=r)), on_vars=on_vars, on_dims=on_dims, on_dim_values=on_dim_values,
104
112
  bins=bins, bins_labels=bins_labels, all_bins=all_bins,
105
- cell_area=cell_area ,dropna=dropna, **stats_kwargs
113
+ cell_area=cell_area, dropna=dropna, **stats_kwargs
106
114
  )
107
115
  df.insert(0, name, regions[r].name)
108
116
  out.append(df)
109
- return pd.concat(out)
117
+ return pd.concat(out)
lsapy/utils.py CHANGED
@@ -1,28 +1,24 @@
1
- """
2
- Miscellaneous Utilities
3
- ===============================
4
-
5
- ...
6
- """
1
+ """Miscellaneous Utilities"""
7
2
 
8
3
  from pathlib import Path
9
4
 
10
5
  import xarray as xr
11
6
 
7
+
12
8
  def _load_data(name: str) -> xr.Dataset:
13
9
  """Load a sample dataset for testing or example purposes."""
14
- if name.lower() not in ['soil', 'climate']:
10
+ if name.lower() not in ["soil", "climate"]:
15
11
  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')
12
+
13
+ data_path = Path(__file__).parent / "data"
14
+ return xr.open_dataset(data_path / f"{name.lower()}_sample.nc")
19
15
 
20
16
 
21
17
  def load_soil_data() -> xr.Dataset:
22
18
  """Load soil dataset for testing or example purposes."""
23
- return _load_data('soil')
19
+ return _load_data("soil")
24
20
 
25
21
 
26
22
  def load_climate_data() -> xr.Dataset:
27
23
  """Load climate dataset for testing or example purposes."""
28
- return _load_data('climate')
24
+ return _load_data("climate")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsapy
3
- Version: 0.1.0.dev0
3
+ Version: 0.1.0.dev1
4
4
  Summary: A Python package for Land Suitability Analysis.
5
5
  Keywords: LSAPy,Land Suitability Analysis,Spatial Analysis,GIS,Geography
6
6
  Author-email: Baptiste Hamon <baptiste.hamon@pg.canterbury.ac.nz>
@@ -0,0 +1,12 @@
1
+ lsapy/__init__.py,sha256=SreXtKw-kHRCBJB4s2JLW4IeJlgQzwpzKMZH4hcdzA8,286
2
+ lsapy/criteria.py,sha256=7VFl1YQcZ015KELsLPsRFFEvGyvaj07Vkj79sM4t7p0,2952
3
+ lsapy/functions.py,sha256=VaQS4ONrKWN5iAXT_xK_Ess1QXvodGsWzlBk8q-PO88,8513
4
+ lsapy/lsa.py,sha256=AewMwvNzYdL3yGBAuncxy35G4oDybMaHqWGfWCxECNo,14459
5
+ lsapy/statistics.py,sha256=lovmy94-km12ObcDj4TlrVpyZDLCvJTqyLHIgRWF0mw,4313
6
+ lsapy/utils.py,sha256=y8-WL2wrPPIHeCwrqXERyY-X71qXnMAosUAmyUGjqU4,732
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.dev1.dist-info/licenses/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
10
+ lsapy-0.1.0.dev1.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
11
+ lsapy-0.1.0.dev1.dist-info/METADATA,sha256=kyssK7TWoRG-yNKHLbfW80y1wXmglG8emgF_KkV-dgQ,1145
12
+ lsapy-0.1.0.dev1.dist-info/RECORD,,
@@ -1,12 +0,0 @@
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,,