lsapy 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.
- lsapy/__init__.py +9 -0
- lsapy/core/__init__.py +1 -0
- lsapy/core/functions.py +52 -0
- lsapy/criteria.py +181 -0
- lsapy/data/climate_sample.nc +0 -0
- lsapy/data/soil_sample.nc +0 -0
- lsapy/functions/__init__.py +7 -0
- lsapy/functions/_discrete.py +30 -0
- lsapy/functions/_suitability.py +158 -0
- lsapy/functions/membership.py +372 -0
- lsapy/lsa.py +624 -0
- lsapy/statistics.py +211 -0
- lsapy/utils.py +56 -0
- lsapy-0.1.0.dist-info/METADATA +187 -0
- lsapy-0.1.0.dist-info/RECORD +17 -0
- lsapy-0.1.0.dist-info/WHEEL +4 -0
- lsapy-0.1.0.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.criteria import SuitabilityCriteria
|
|
4
|
+
from lsapy.functions import *
|
|
5
|
+
from lsapy.lsa import LandSuitabilityAnalysis
|
|
6
|
+
|
|
7
|
+
__author__ = "Baptiste Hamon"
|
|
8
|
+
__email__ = "baptiste.hamon@pg.canterbury.ac.nz"
|
|
9
|
+
__version__ = "0.1.0"
|
lsapy/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core Module."""
|
lsapy/core/functions.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Suitability Function Utilities."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
equations: dict[str, dict] = {}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def declare_equation(type: str, alt_name: str | None = None) -> Callable:
|
|
9
|
+
"""
|
|
10
|
+
Register an equation in the `equations` mapping under the specified type.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
type : str
|
|
15
|
+
The type of equation to register.
|
|
16
|
+
alt_name : str, optional
|
|
17
|
+
An alternative name for the equation (not currently supported).
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
Callable
|
|
22
|
+
A decorator function that registers the equation.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# TODO: Support alternative names for equations
|
|
26
|
+
def _decorator(func: callable):
|
|
27
|
+
if type not in equations:
|
|
28
|
+
equations[type] = {}
|
|
29
|
+
equations[type].update({func.__name__: func})
|
|
30
|
+
return func
|
|
31
|
+
|
|
32
|
+
return _decorator
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_function_from_name(name: str) -> Callable:
|
|
36
|
+
"""
|
|
37
|
+
Retrieve a function by its name from the registered equations.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
name : str
|
|
42
|
+
The name of the function to retrieve.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
Callable
|
|
47
|
+
The function corresponding to the given name.
|
|
48
|
+
"""
|
|
49
|
+
for _, funcs in equations.items():
|
|
50
|
+
if name in funcs:
|
|
51
|
+
return funcs[name]
|
|
52
|
+
raise ValueError(f"Equation `{name}` not implemented.")
|
lsapy/criteria.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Suitability Criteria definition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
import xarray as xr
|
|
8
|
+
|
|
9
|
+
from lsapy.functions import SuitabilityFunction
|
|
10
|
+
|
|
11
|
+
__all__ = ["SuitabilityCriteria"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SuitabilityCriteria:
|
|
15
|
+
"""
|
|
16
|
+
A data structure for suitability criteria.
|
|
17
|
+
|
|
18
|
+
Suitability criteria are used to compute the suitability of a location from an indicator and based on a set of rules
|
|
19
|
+
defined by a suitability function. The suitability criteria can be weighted and categorized defining how it will be
|
|
20
|
+
aggregated with other criteria.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
name : str
|
|
25
|
+
Name of the suitability criteria.
|
|
26
|
+
indicator : xr.DataArray
|
|
27
|
+
Indicator on which the criteria is based.
|
|
28
|
+
func : SuitabilityFunction | MembershipFunction | DiscreteSuitFunction
|
|
29
|
+
Suitability function describing how the suitability of the criteria is computed.
|
|
30
|
+
weight : int | float | None, optional
|
|
31
|
+
Weight of the criteria used in the aggregation process if a weighted aggregation method is used.
|
|
32
|
+
The default is 1.
|
|
33
|
+
category : str | None, optional
|
|
34
|
+
Category of the criteria. The default is None.
|
|
35
|
+
long_name : str | None, optional
|
|
36
|
+
A long name for the criteria. The default is None.
|
|
37
|
+
description : str | None, optional
|
|
38
|
+
A description for the criteria. The default is None.
|
|
39
|
+
comment : str | None, optional
|
|
40
|
+
Additional information about the criteria.
|
|
41
|
+
is_computed : bool, optional
|
|
42
|
+
If the indicator data already contains the computed suitability values. Default is False.
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
Here is an example using the sample soil data with the drainage class (DRC) as indicator for the criteria.
|
|
47
|
+
|
|
48
|
+
>>> from lsapy.utils import load_soil_data, load_climate_data
|
|
49
|
+
>>> from lsapy.functions import SuitabilityFunction
|
|
50
|
+
>>> from xclim.indicators.atmos import growing_degree_days
|
|
51
|
+
|
|
52
|
+
>>> soil_data = load_soil_data()
|
|
53
|
+
>>> sc = SuitabilityCriteria(
|
|
54
|
+
... name="drainage_class",
|
|
55
|
+
... long_name="Drainage Class Suitability",
|
|
56
|
+
... weight=3,
|
|
57
|
+
... category="soilTerrain",
|
|
58
|
+
... indicator=soil_data["DRC"],
|
|
59
|
+
... func=SuitabilityFunction(name="discrete", params={"rules": {"1": 0, "2": 0.1, "3": 0.5, "4": 0.9, "5": 1}}),
|
|
60
|
+
... )
|
|
61
|
+
|
|
62
|
+
Here is another example using the sample climate data with the growing degree days (GDD)
|
|
63
|
+
as indicator for the criteria computing using the `xclim` package.
|
|
64
|
+
|
|
65
|
+
>>> clim_data = load_climate_data()
|
|
66
|
+
>>> gdd = growing_degree_days(clim_data["tas"], thresh="10 degC", freq="YS-JUL")
|
|
67
|
+
>>> sc = SuitabilityCriteria(
|
|
68
|
+
... name="growing_degree_days",
|
|
69
|
+
... long_name="Growing Degree Days Suitability",
|
|
70
|
+
... weight=1,
|
|
71
|
+
... category="climate",
|
|
72
|
+
... indicator=gdd,
|
|
73
|
+
... func=SuitabilityFunction(name="vetharaniam2022_eq5", params={"a": -1.41, "b": 801}),
|
|
74
|
+
... )
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
name: str,
|
|
80
|
+
indicator: xr.DataArray,
|
|
81
|
+
func: SuitabilityFunction | Callable = None,
|
|
82
|
+
weight: int | float | None = 1,
|
|
83
|
+
category: str | None = None,
|
|
84
|
+
long_name: str | None = None,
|
|
85
|
+
description: str | None = None,
|
|
86
|
+
comment: str | None = None,
|
|
87
|
+
is_computed: bool = False,
|
|
88
|
+
) -> None:
|
|
89
|
+
self.name = name
|
|
90
|
+
self.indicator = indicator
|
|
91
|
+
self.func = func
|
|
92
|
+
self.weight = weight
|
|
93
|
+
self.category = category
|
|
94
|
+
self.long_name = long_name
|
|
95
|
+
self.description = description
|
|
96
|
+
self.comment = comment
|
|
97
|
+
self.is_computed = is_computed
|
|
98
|
+
self._from_indicator = _get_indicator_description(indicator)
|
|
99
|
+
|
|
100
|
+
def __repr__(self) -> str:
|
|
101
|
+
"""Return a string representation for a particular criteria."""
|
|
102
|
+
attrs = []
|
|
103
|
+
attrs.append(f"name='{self.name}'")
|
|
104
|
+
attrs.append(f"indicator={self.indicator.name}")
|
|
105
|
+
attrs.append(f"func={self.func if self.func is not None else 'unknown'}")
|
|
106
|
+
attrs.append(f"weight={self.weight}")
|
|
107
|
+
if self.category is not None:
|
|
108
|
+
attrs.append(f"category='{self.category}'")
|
|
109
|
+
if self.long_name is not None:
|
|
110
|
+
attrs.append(f"long_name='{self.long_name}'")
|
|
111
|
+
if self.description is not None:
|
|
112
|
+
attrs.append(f"description='{self.description}'")
|
|
113
|
+
if self.comment is not None:
|
|
114
|
+
attrs.append(f"comment='{self.comment}'")
|
|
115
|
+
if self.is_computed:
|
|
116
|
+
attrs.append("is_computed=True")
|
|
117
|
+
return f"{self.__class__.__name__}({', '.join(attrs) if attrs else ''})"
|
|
118
|
+
|
|
119
|
+
def compute(self) -> xr.DataArray:
|
|
120
|
+
"""
|
|
121
|
+
Compute the suitability of the criteria.
|
|
122
|
+
|
|
123
|
+
Returns a xarray DataArray with criteria suitability. The attributes of the DataArray describe how
|
|
124
|
+
the suitability was computed.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
xr.DataArray
|
|
129
|
+
Criteria suitability.
|
|
130
|
+
"""
|
|
131
|
+
if self.is_computed:
|
|
132
|
+
sc = self.indicator
|
|
133
|
+
elif self.func is None:
|
|
134
|
+
raise ValueError("The suitability function is not defined. Please provide a valid function.")
|
|
135
|
+
else:
|
|
136
|
+
sc: xr.DataArray = xr.apply_ufunc(self.func, self.indicator)
|
|
137
|
+
return sc.rename(self.name).assign_attrs(
|
|
138
|
+
dict(
|
|
139
|
+
{
|
|
140
|
+
k: v
|
|
141
|
+
for k, v in self.attrs.items()
|
|
142
|
+
if k not in ["name", "func_method", "from_indicator", "is_computed"]
|
|
143
|
+
},
|
|
144
|
+
**{
|
|
145
|
+
"history": f"func_method: {self.func if self.func is not None else 'unknown'}; "
|
|
146
|
+
f"from_indicator: [{self._from_indicator}]"
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def attrs(self) -> dict:
|
|
153
|
+
"""
|
|
154
|
+
Dictionary of the criteria attributes.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
dict
|
|
159
|
+
Dictionary with the criteria attributes.
|
|
160
|
+
"""
|
|
161
|
+
return {
|
|
162
|
+
k: v
|
|
163
|
+
for k, v in {
|
|
164
|
+
"name": self.name,
|
|
165
|
+
"weight": self.weight,
|
|
166
|
+
"category": self.category,
|
|
167
|
+
"long_name": self.long_name,
|
|
168
|
+
"description": self.description,
|
|
169
|
+
"func_method": self.func if self.func is not None else "unknown",
|
|
170
|
+
"from_indicator": self._from_indicator,
|
|
171
|
+
"is_computed": self.is_computed,
|
|
172
|
+
}.items()
|
|
173
|
+
if v is not None
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_indicator_description(indicator: xr.Dataset | xr.DataArray) -> str:
|
|
178
|
+
if indicator.attrs != {}:
|
|
179
|
+
return f"name: {indicator.name}; " + "; ".join([f"{k}: {v}" for k, v in indicator.attrs.items()])
|
|
180
|
+
else:
|
|
181
|
+
return f"name: {indicator.name}"
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Discrete Function Module."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from lsapy.core.functions import declare_equation
|
|
6
|
+
|
|
7
|
+
__all__ = ["discrete"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@declare_equation("categorical")
|
|
11
|
+
def discrete(x, rules: dict[str | int, int | float]) -> np.ndarray:
|
|
12
|
+
"""
|
|
13
|
+
Discrete suitability function.
|
|
14
|
+
|
|
15
|
+
This function maps the indicator values to a set of rules that define the suitability values.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
x : any
|
|
20
|
+
Indicator values to map.
|
|
21
|
+
rules : dict[str | int, int | float]
|
|
22
|
+
Rules to map the indicator values to suitability values. The keys correspond to the indicator values and the
|
|
23
|
+
values to its associated suitability values.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
np.ndarray
|
|
28
|
+
Suitability values.
|
|
29
|
+
"""
|
|
30
|
+
return np.vectorize(rules.get, otypes=[np.float32])(x, np.nan)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Suitability Function Module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from lsapy.core.functions import get_function_from_name
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SuitabilityFunction",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SuitabilityFunction:
|
|
20
|
+
"""
|
|
21
|
+
Suitability Function.
|
|
22
|
+
|
|
23
|
+
Suitability function define how the criteria indicator is transformed into a suitability value. The suitability
|
|
24
|
+
function are available for continuous and discrete indicators. For continuous indicators, a membership function
|
|
25
|
+
is applied to convert indicator values into a suitability. For discrete indicators, a set of rules is mapped
|
|
26
|
+
on the indicator.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
func : Callable | None, optional
|
|
31
|
+
Function to compute the suitability value.
|
|
32
|
+
name : str | None, optional
|
|
33
|
+
Name of the implemented function to use. If `func` is provided, this parameter is ignored.
|
|
34
|
+
params : dict[str, Any], optional
|
|
35
|
+
Parameters of the function.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> func = SuitabilityFunction(name="logistic", params={"a": 1, "b": 5})
|
|
40
|
+
>>> func(3)
|
|
41
|
+
array(0.11920292, dtype=float32)
|
|
42
|
+
|
|
43
|
+
``SuitabilityFunction`` can also be used for discrete functions.
|
|
44
|
+
|
|
45
|
+
>>> func = SuitabilityFunction(name="discrete", params={"rules": {1: 0, 2: 0.1, 3: 0.5, 4: 0.9, 5: 1}})
|
|
46
|
+
>>> func(3)
|
|
47
|
+
array(0.5, dtype=float32)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, func: Callable | None = None, name: str | None = None, params: dict[str, Any] | None = None):
|
|
51
|
+
if func is None:
|
|
52
|
+
if name is None:
|
|
53
|
+
raise ValueError("Either `func` or `name` must be provided to define the suitability function.")
|
|
54
|
+
elif not isinstance(name, str):
|
|
55
|
+
raise TypeError("`name` must be a string when `func` is not provided.")
|
|
56
|
+
else:
|
|
57
|
+
self.func = get_function_from_name(name)
|
|
58
|
+
else:
|
|
59
|
+
if not isinstance(func, Callable):
|
|
60
|
+
raise TypeError("`func` must be a callable function.")
|
|
61
|
+
if name is not None:
|
|
62
|
+
warnings.warn("`name` is ignored when `func` is provided", stacklevel=2)
|
|
63
|
+
self.func = func
|
|
64
|
+
|
|
65
|
+
self.params = params
|
|
66
|
+
|
|
67
|
+
def __call__(self, x):
|
|
68
|
+
"""Call the suitability function."""
|
|
69
|
+
if self.func is None:
|
|
70
|
+
raise ValueError("No function has been provided.")
|
|
71
|
+
if self.params:
|
|
72
|
+
|
|
73
|
+
def func(val):
|
|
74
|
+
"""Wrap the function to include parameters."""
|
|
75
|
+
return self.func(val, **self.params)
|
|
76
|
+
else:
|
|
77
|
+
|
|
78
|
+
def func(val):
|
|
79
|
+
"""Wrap the function without parameters."""
|
|
80
|
+
return self.func(val)
|
|
81
|
+
|
|
82
|
+
return np.vectorize(func, otypes=[np.float32])(x)
|
|
83
|
+
|
|
84
|
+
def __repr__(self):
|
|
85
|
+
"""Return the string representation of the object."""
|
|
86
|
+
return f"{self.__class__.__name__}(func={self.func.__name__}, params={self.params})"
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def attrs(self) -> dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
Dictionary of the suitability function attributes.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
dict
|
|
96
|
+
Dictionary containing the function name and parameters. If both are undefined, an empty dictionary
|
|
97
|
+
is returned.
|
|
98
|
+
"""
|
|
99
|
+
return {k: v for k, v in {"func": self.func, "params": self.params}.items() if v is not None}
|
|
100
|
+
|
|
101
|
+
def map(self, x):
|
|
102
|
+
"""
|
|
103
|
+
Map the suitability function.
|
|
104
|
+
|
|
105
|
+
This method converts the input values into suitability values for the defined function.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
x : any
|
|
110
|
+
Input values to map.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
any
|
|
115
|
+
Suitability values.
|
|
116
|
+
|
|
117
|
+
Raises
|
|
118
|
+
------
|
|
119
|
+
ValueError
|
|
120
|
+
If no function has been provided.
|
|
121
|
+
|
|
122
|
+
Examples
|
|
123
|
+
--------
|
|
124
|
+
>>> from lsapy.functions import logistic
|
|
125
|
+
|
|
126
|
+
>>> sf = SuitabilityFunction(func=logistic, params={"a": 1, "b": 5})
|
|
127
|
+
>>> sf.map(3)
|
|
128
|
+
array(0.11920292, dtype=float32)
|
|
129
|
+
|
|
130
|
+
.. deprecated:: 0.1.0-dev2
|
|
131
|
+
`map` will be removed in LSAPy 0.1.0 because it is redundant with the `__call__` method.
|
|
132
|
+
Please use the `__call__` method directly instead.
|
|
133
|
+
"""
|
|
134
|
+
warnings.warn(
|
|
135
|
+
"`map` is deprecated and will be removed in LSAPy 0.1.0. Use `__call__` directly instead.",
|
|
136
|
+
FutureWarning,
|
|
137
|
+
stacklevel=2,
|
|
138
|
+
)
|
|
139
|
+
return self(x)
|
|
140
|
+
|
|
141
|
+
def plot(self, x) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Basic plot of the suitability function.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
x : any
|
|
148
|
+
Input values to plot.
|
|
149
|
+
|
|
150
|
+
Examples
|
|
151
|
+
--------
|
|
152
|
+
>>> import numpy as np # doctest: +SKIP
|
|
153
|
+
>>> from lsapy.functions import logistic
|
|
154
|
+
|
|
155
|
+
>>> sf = SuitabilityFunction(func=logistic, params={"a": 1, "b": 5})
|
|
156
|
+
>>> sf.plot(np.linspace(0, 10, 100)) # doctest: +SKIP
|
|
157
|
+
"""
|
|
158
|
+
plt.plot(x, self(x))
|