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 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."""
@@ -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,7 @@
1
+ """Suitability Functions Module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from lsapy.functions._discrete import *
6
+ from lsapy.functions._suitability import SuitabilityFunction
7
+ from lsapy.functions.membership import *
@@ -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))