wolfhece 2.2.38__py3-none-any.whl → 2.2.39__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.
- wolfhece/Coordinates_operations.py +5 -0
- wolfhece/GraphNotebook.py +72 -1
- wolfhece/GraphProfile.py +1 -1
- wolfhece/MulticriteriAnalysis.py +1579 -0
- wolfhece/PandasGrid.py +62 -1
- wolfhece/PyCrosssections.py +194 -43
- wolfhece/PyDraw.py +891 -73
- wolfhece/PyGui.py +913 -72
- wolfhece/PyGuiHydrology.py +528 -74
- wolfhece/PyPalette.py +26 -4
- wolfhece/PyParams.py +33 -0
- wolfhece/PyPictures.py +2 -2
- wolfhece/PyVertex.py +25 -0
- wolfhece/PyVertexvectors.py +94 -28
- wolfhece/PyWMS.py +52 -36
- wolfhece/acceptability/acceptability.py +15 -8
- wolfhece/acceptability/acceptability_gui.py +507 -360
- wolfhece/acceptability/func.py +80 -183
- wolfhece/apps/version.py +1 -1
- wolfhece/compare_series.py +480 -0
- wolfhece/drawing_obj.py +12 -1
- wolfhece/hydrology/Catchment.py +228 -162
- wolfhece/hydrology/Internal_variables.py +43 -2
- wolfhece/hydrology/Models_characteristics.py +69 -67
- wolfhece/hydrology/Optimisation.py +893 -182
- wolfhece/hydrology/PyWatershed.py +267 -165
- wolfhece/hydrology/SubBasin.py +185 -140
- wolfhece/hydrology/cst_exchanges.py +76 -1
- wolfhece/hydrology/forcedexchanges.py +413 -49
- wolfhece/hydrology/read.py +65 -5
- wolfhece/hydrometry/kiwis.py +14 -7
- wolfhece/insyde_be/INBE_func.py +746 -0
- wolfhece/insyde_be/INBE_gui.py +1776 -0
- wolfhece/insyde_be/__init__.py +3 -0
- wolfhece/interpolating_raster.py +366 -0
- wolfhece/irm_alaro.py +1457 -0
- wolfhece/irm_qdf.py +889 -57
- wolfhece/lifewatch.py +6 -3
- wolfhece/picc.py +124 -8
- wolfhece/pyLandUseFlanders.py +146 -0
- wolfhece/pydownloader.py +2 -1
- wolfhece/pywalous.py +225 -31
- wolfhece/toolshydrology_dll.py +149 -0
- wolfhece/wolf_array.py +63 -25
- {wolfhece-2.2.38.dist-info → wolfhece-2.2.39.dist-info}/METADATA +3 -1
- {wolfhece-2.2.38.dist-info → wolfhece-2.2.39.dist-info}/RECORD +49 -40
- {wolfhece-2.2.38.dist-info → wolfhece-2.2.39.dist-info}/WHEEL +0 -0
- {wolfhece-2.2.38.dist-info → wolfhece-2.2.39.dist-info}/entry_points.txt +0 -0
- {wolfhece-2.2.38.dist-info → wolfhece-2.2.39.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1579 @@
|
|
1
|
+
"""
|
2
|
+
This module contains the objects needed to perform
|
3
|
+
a multi-criteria analysis for fish passage
|
4
|
+
on spatially distributed hydrodynamic data.
|
5
|
+
|
6
|
+
The module tests are located in the folder:
|
7
|
+
tests/test_MulticriteriAnalysis.py file,
|
8
|
+
in HECEPython repository.
|
9
|
+
|
10
|
+
Authors: Utashi Ciraane,
|
11
|
+
HECE, University of Liège, Belgium.
|
12
|
+
"""
|
13
|
+
# **************************************************************
|
14
|
+
# Libraries used throughout the module
|
15
|
+
# **************************************************************
|
16
|
+
|
17
|
+
# To stay inside the HECEPython repository
|
18
|
+
# ----------------------------------------
|
19
|
+
|
20
|
+
# Standard libraries
|
21
|
+
# -------------------
|
22
|
+
import logging
|
23
|
+
import numpy as np
|
24
|
+
import os
|
25
|
+
|
26
|
+
# Cherry-picked objects from standard libraries
|
27
|
+
# ---------------------------------------------
|
28
|
+
from enum import Enum
|
29
|
+
from pathlib import Path
|
30
|
+
from tqdm import tqdm
|
31
|
+
from typing import Literal, Union
|
32
|
+
|
33
|
+
# Cherry-picked objects from wolfhece
|
34
|
+
# ------------------------------------
|
35
|
+
from wolfhece.wolf_array import WolfArray, header_wolf
|
36
|
+
|
37
|
+
## **************************************************************
|
38
|
+
# Constants Used
|
39
|
+
# **************************************************************
|
40
|
+
class Operator(Enum):
|
41
|
+
"""
|
42
|
+
Mathematical operators used
|
43
|
+
in the module.
|
44
|
+
"""
|
45
|
+
AVERAGE = "average"
|
46
|
+
BETWEEN = "between" # Between two limits. The limits are included.
|
47
|
+
BETWEEN_STRICT = "strictly between" # Between two limits. The limits are not included.
|
48
|
+
EQUAL = "equal" # Equal to a value.
|
49
|
+
INFERIOR = "inferior" # Less than a limit. The limit is not included.
|
50
|
+
INFERIOR_OR_EQUAL = "inferior or equal" # Less than or equal to a limit. The limit is included.
|
51
|
+
OUTSIDE = "outside" # Outside two limits. The limits are included.
|
52
|
+
OUTSIDE_STRICT = "strictly outside" # Outside two limits. The limits are not included.
|
53
|
+
PERCENTAGE = "percentage"
|
54
|
+
PRODUCT = "product"
|
55
|
+
SUM = "sum"
|
56
|
+
SUPERIOR = "superior" # Greater than a limit. The limit is not included.
|
57
|
+
SUPERIOR_OR_EQUAL = "superior or equal" # Greater than or equal to a limit. The limit is included.
|
58
|
+
THRESHOLD = "threshold"
|
59
|
+
WEIGHTED_SUM = "weighted sum"
|
60
|
+
|
61
|
+
class Format(Enum):
|
62
|
+
"""
|
63
|
+
Types of data formats used in the module.
|
64
|
+
"""
|
65
|
+
FLOAT32 = np.float32 # The float32 data type used in the module.
|
66
|
+
FLOAT64 = np.float64 # The float64 data type used in the module.
|
67
|
+
INT32 = np.int32 # The int32 data type used in the module.
|
68
|
+
INT64 = np.int64 # The int64 data type used in the module.
|
69
|
+
|
70
|
+
class Status(Enum):
|
71
|
+
"""
|
72
|
+
Status of objects in the module
|
73
|
+
"""
|
74
|
+
PROGRESS_BAR_DISABLE = False # Enable or disable the tqdm progress bars.
|
75
|
+
|
76
|
+
|
77
|
+
class Constant(Enum):
|
78
|
+
"""
|
79
|
+
This class contains the constants used throughout the module.
|
80
|
+
"""
|
81
|
+
EPSG = 31370
|
82
|
+
ONE = 1
|
83
|
+
ZERO = 0
|
84
|
+
CONDITION = "condition"
|
85
|
+
|
86
|
+
|
87
|
+
## **************************************************************
|
88
|
+
# Classes or Objects used to perform the multi-criteria analysis
|
89
|
+
## **************************************************************
|
90
|
+
class Variable:
|
91
|
+
"""
|
92
|
+
This class is used to store variable (data: array)
|
93
|
+
that will be used in the multi-criteria analysis.
|
94
|
+
|
95
|
+
The types of variable allowed are:
|
96
|
+
- np.ma.MaskedArray: A numpy masked array.
|
97
|
+
- np.ndarray: A numpy array.
|
98
|
+
- WolfArray: A WolfArray object from the wolfhece library.
|
99
|
+
"""
|
100
|
+
def __init__(self,
|
101
|
+
variable: np.ma.MaskedArray| np.ndarray| WolfArray,
|
102
|
+
dtype : Union[np.dtype, str] = Format.FLOAT32.value) :
|
103
|
+
"""
|
104
|
+
Initialize the variable with a WolfArray or a numpy array.
|
105
|
+
if a type dtype is not provided, it defaults to np.float32.
|
106
|
+
|
107
|
+
:param variable: A WolfArray or a numpy array.
|
108
|
+
::type variable: np.ma.MaskedArray | np.ndarray | WolfArray
|
109
|
+
:param dtype: The data type of the variable, defaults to np.float32.
|
110
|
+
::type dtype: Union[np.dtype, str]
|
111
|
+
|
112
|
+
"""
|
113
|
+
self._variable: np.ma.MaskedArray = None
|
114
|
+
self._dtype = dtype
|
115
|
+
self.variable = variable
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
@property
|
120
|
+
def variable(self) -> np.ma.MaskedArray:
|
121
|
+
"""
|
122
|
+
Return the variable.
|
123
|
+
|
124
|
+
:return: The variable as a numpy masked array.
|
125
|
+
"""
|
126
|
+
return self._variable
|
127
|
+
|
128
|
+
@variable.setter
|
129
|
+
def variable(self, value: np.ma.MaskedArray| np.ndarray| WolfArray) :
|
130
|
+
"""
|
131
|
+
Set the variable and converts it to a numpy masked array.
|
132
|
+
|
133
|
+
:param value: A WolfArray or a numpy masked array.
|
134
|
+
"""
|
135
|
+
assert isinstance(value, (np.ma.MaskedArray, np.ndarray, WolfArray)), \
|
136
|
+
"The variable must be a numpy masked array, a numpy array or a WolfArray."
|
137
|
+
if isinstance(value, WolfArray):
|
138
|
+
if np.ma.is_masked(value.array):
|
139
|
+
value = value.array
|
140
|
+
else:
|
141
|
+
# If the WolfArray is not masked, create a masked array with no mask.
|
142
|
+
mask = np.zeros(value.array.shape, dtype=bool)
|
143
|
+
value = np.ma.masked_array(value.array, mask=mask)
|
144
|
+
|
145
|
+
elif isinstance(value, np.ndarray):
|
146
|
+
mask =np.zeros(value.shape, dtype=bool)
|
147
|
+
value = np.ma.masked_array(value, mask=mask)
|
148
|
+
self._variable = value.astype(self._dtype, copy=True)
|
149
|
+
|
150
|
+
@property
|
151
|
+
def dtype(self) -> type:
|
152
|
+
"""
|
153
|
+
Return the data type of the variable.
|
154
|
+
|
155
|
+
:return: The data type of the variable.
|
156
|
+
"""
|
157
|
+
return self._variable.dtype
|
158
|
+
|
159
|
+
@dtype.setter
|
160
|
+
def dtype(self, value: Union[type, str]) -> None:
|
161
|
+
"""
|
162
|
+
Set the data type of the variable.
|
163
|
+
|
164
|
+
:param value: The data type to set.
|
165
|
+
"""
|
166
|
+
assert isinstance(value, (type, str)), \
|
167
|
+
"The data type must be a numpy dtype or a string representing a numpy dtype."
|
168
|
+
# if isinstance(value, str):
|
169
|
+
# # value = type(value)
|
170
|
+
|
171
|
+
self._variable = self._variable.astype(value, copy=True)
|
172
|
+
self._dtype = value
|
173
|
+
|
174
|
+
class Criteria:
|
175
|
+
"""
|
176
|
+
This class is used to define a criteria included in the multi-criteria analysis
|
177
|
+
|
178
|
+
It contains a threshold and a condition:.
|
179
|
+
- superior,
|
180
|
+
- inferior,
|
181
|
+
- equal,
|
182
|
+
- superior or equal,
|
183
|
+
- inferior or equal,
|
184
|
+
- between,
|
185
|
+
- strictly between,
|
186
|
+
- outside,
|
187
|
+
- strictly outside.
|
188
|
+
|
189
|
+
The condition defines how the threshold is applied to select
|
190
|
+
values in the variable true (1) or False (0).
|
191
|
+
"""
|
192
|
+
def __init__(self,
|
193
|
+
threshold: Union[float, int, tuple],
|
194
|
+
condition: Literal[Operator.SUPERIOR.value,
|
195
|
+
Operator.INFERIOR.value,
|
196
|
+
Operator.SUPERIOR_OR_EQUAL.value,
|
197
|
+
Operator.INFERIOR_OR_EQUAL.value,
|
198
|
+
Operator.BETWEEN.value,
|
199
|
+
Operator.BETWEEN_STRICT.value,
|
200
|
+
Operator.OUTSIDE.value,
|
201
|
+
Operator.OUTSIDE_STRICT.value
|
202
|
+
] = Operator.SUPERIOR_OR_EQUAL.value) -> dict:
|
203
|
+
"""
|
204
|
+
Return a criteria with a threshold and a condition.
|
205
|
+
|
206
|
+
:param threshold: The threshold value for the criteria.
|
207
|
+
:type threshold: float| int|tuple
|
208
|
+
:param condition: The condition to select the data around the threshold.
|
209
|
+
:type condition: str
|
210
|
+
:return: a dictionary containing 2 elements, the threshold and the condition.
|
211
|
+
:rtype: dict
|
212
|
+
"""
|
213
|
+
|
214
|
+
self._available_conditions = [Operator.SUPERIOR.value,
|
215
|
+
Operator.INFERIOR.value,
|
216
|
+
Operator.EQUAL.value,
|
217
|
+
Operator.SUPERIOR_OR_EQUAL.value,
|
218
|
+
Operator.INFERIOR_OR_EQUAL.value,
|
219
|
+
Operator.BETWEEN.value,
|
220
|
+
Operator.BETWEEN_STRICT.value,
|
221
|
+
Operator.OUTSIDE.value,
|
222
|
+
Operator.OUTSIDE_STRICT.value
|
223
|
+
] # add NOT in method too.
|
224
|
+
self._needs_tuple = (Operator.BETWEEN.value,
|
225
|
+
Operator.BETWEEN_STRICT.value,
|
226
|
+
Operator.OUTSIDE.value,
|
227
|
+
Operator.OUTSIDE_STRICT.value)
|
228
|
+
self._criteria: dict = {}
|
229
|
+
|
230
|
+
self.threshold = threshold
|
231
|
+
self.condition = condition
|
232
|
+
|
233
|
+
@property
|
234
|
+
def criteria(self) -> dict:
|
235
|
+
"""
|
236
|
+
Return the criteria.
|
237
|
+
|
238
|
+
:return: The criteria as a dictionary.
|
239
|
+
"""
|
240
|
+
return self._criteria
|
241
|
+
|
242
|
+
@criteria.setter
|
243
|
+
def criteria(self,
|
244
|
+
criteria : dict) -> None:
|
245
|
+
"""
|
246
|
+
Set the criteria.
|
247
|
+
|
248
|
+
:param value: The criteria as a dictionary.
|
249
|
+
"""
|
250
|
+
assert isinstance(criteria, dict), f"The criteria must be a dictionary, not {type(criteria)}."
|
251
|
+
assert Operator.THRESHOLD.value in criteria,\
|
252
|
+
f"The threshold key must be {Operator.THRESHOLD.value } in the criteria dictionary."
|
253
|
+
assert Constant.CONDITION.value in criteria,\
|
254
|
+
f"The condition key must be {Constant.CONDITION.value} in the criteria dictionary."
|
255
|
+
self.threshold = criteria[Operator.THRESHOLD.value]
|
256
|
+
self.condition = criteria[Constant.CONDITION.value]
|
257
|
+
|
258
|
+
@property
|
259
|
+
def threshold(self) -> float:
|
260
|
+
"""
|
261
|
+
Get the threshold of the criteria.
|
262
|
+
|
263
|
+
:return: The threshold as a float.
|
264
|
+
"""
|
265
|
+
return self._criteria[Operator.THRESHOLD.value]
|
266
|
+
|
267
|
+
@threshold.setter
|
268
|
+
def threshold(self, value: float) -> None:
|
269
|
+
"""
|
270
|
+
Set the threshold of the criteria.
|
271
|
+
|
272
|
+
:param value: The threshold to set.
|
273
|
+
"""
|
274
|
+
assert isinstance(value, (float, int, tuple)),\
|
275
|
+
f"The threshold must be a float or an int or a tuple of length equals to Two, not a {type(value)} ."
|
276
|
+
if isinstance(value, tuple):
|
277
|
+
assert len(value) == 2, "The threshold tuple must have exactly two elements."
|
278
|
+
assert value[0] <= value[1], "The first element of the threshold tuple must be less than the second element."
|
279
|
+
if Constant.CONDITION.value in self._criteria:
|
280
|
+
if self.condition not in (self._needs_tuple):
|
281
|
+
value = self._tuple_to_number(value)
|
282
|
+
logging.warning(f"The condition '{self.condition}' does not require a tuple for the threshold. "
|
283
|
+
f"The threshold will be set to : {value} which i the first value of the tuple.")
|
284
|
+
|
285
|
+
elif isinstance(value, (float, int)):
|
286
|
+
if Constant.CONDITION.value in self._criteria:
|
287
|
+
if self.condition in self._needs_tuple:
|
288
|
+
logging.warning(f"The condition '{self.condition}' requires a tuple for the threshold. "
|
289
|
+
f"The threshold will be set to a tuple with the value {value} as both elements.")
|
290
|
+
# Convert single number to tuple
|
291
|
+
value = self._number_to_tuple(value)
|
292
|
+
assert isinstance(self._criteria, dict), f"The criteria type must be a dictionary, not {type(self._criteria)}."
|
293
|
+
self._criteria[Operator.THRESHOLD.value] = value
|
294
|
+
|
295
|
+
@property
|
296
|
+
def condition(self) -> str:
|
297
|
+
"""
|
298
|
+
Get the condition.
|
299
|
+
|
300
|
+
:return: The condition as a string.
|
301
|
+
"""
|
302
|
+
return self._criteria[Constant.CONDITION.value]
|
303
|
+
|
304
|
+
@condition.setter
|
305
|
+
def condition(self, value: Literal[Operator.SUPERIOR.value,
|
306
|
+
Operator.INFERIOR.value,
|
307
|
+
Operator.EQUAL.value,
|
308
|
+
Operator.SUPERIOR_OR_EQUAL.value,
|
309
|
+
Operator.INFERIOR_OR_EQUAL.value,
|
310
|
+
Operator.BETWEEN.value,
|
311
|
+
Operator.BETWEEN_STRICT.value
|
312
|
+
]) -> None:
|
313
|
+
"""
|
314
|
+
Set the condition.
|
315
|
+
|
316
|
+
:param value: The condition to set.
|
317
|
+
"""
|
318
|
+
assert isinstance(value, str), f"The condition must be a string, not a {type(value)}."
|
319
|
+
assert value in self._available_conditions,\
|
320
|
+
f"The criteria must be one of the following: {self._available_conditions}."
|
321
|
+
self._criteria[Constant.CONDITION.value] = value
|
322
|
+
# If the condition is a range condition (between, strictly between, outside, strictly outside),
|
323
|
+
# we need to convert the threshold to a tuple if it is not already.
|
324
|
+
if value in self._needs_tuple:
|
325
|
+
if isinstance(self.threshold, (int, float)):
|
326
|
+
# If the threshold is a single number, convert it to a tuple
|
327
|
+
self.threshold = self._number_to_tuple(self.threshold)
|
328
|
+
else:
|
329
|
+
# If the condition does not require a tuple, ensure the threshold is a single number
|
330
|
+
if isinstance(self.threshold, tuple):
|
331
|
+
self.threshold = self._tuple_to_number(self.threshold)
|
332
|
+
|
333
|
+
def _number_to_tuple(self, value: Union[float, int]) -> tuple:
|
334
|
+
"""
|
335
|
+
Convert a number to a tuple of two elements.
|
336
|
+
|
337
|
+
:param value: The number to convert.
|
338
|
+
:return: A tuple with the number as both elements.
|
339
|
+
"""
|
340
|
+
assert isinstance(value, (float, int)), f"The value must be a float or an int, not {type(value)}."
|
341
|
+
return (value, value)
|
342
|
+
|
343
|
+
def _tuple_to_number(self, value: tuple) -> float:
|
344
|
+
"""
|
345
|
+
Convert a tuple of two elements to a number.
|
346
|
+
|
347
|
+
:param value: The tuple to convert.
|
348
|
+
:return: The first element of the tuple as a float.
|
349
|
+
"""
|
350
|
+
assert isinstance(value, tuple), f"The value must be a tuple, not {type(value)}."
|
351
|
+
assert len(value) == 2, "The tuple must have exactly two elements."
|
352
|
+
return value[0]
|
353
|
+
|
354
|
+
class Score:
|
355
|
+
"""
|
356
|
+
This class scores a Variable based on the given criteria (threshold & condition).
|
357
|
+
|
358
|
+
The score is a np.MaskedArray with the same shape as the given variable.
|
359
|
+
|
360
|
+
The score contains boolean values indicating whether the variable meets the criteria (1) or (0).
|
361
|
+
If the variable is masked, the score is also masked.
|
362
|
+
|
363
|
+
Binary notation was selected to allow mathematical operations.
|
364
|
+
"""
|
365
|
+
def __init__(self,
|
366
|
+
variable: Variable,
|
367
|
+
criteria: Criteria,
|
368
|
+
dtype = Format.INT32.value
|
369
|
+
) -> None:
|
370
|
+
"""
|
371
|
+
Initialize the score with a variable and a criteria.
|
372
|
+
|
373
|
+
:param variable: The variable (array) to score.
|
374
|
+
:type variable: Variable
|
375
|
+
:param criteria: The criteria to use for scoring (threshold and condition).
|
376
|
+
:type criteria: Criteria
|
377
|
+
:param dtype: The data type of the score, defaults to np.int32.
|
378
|
+
:type dtype: Union[np.dtype, str]
|
379
|
+
"""
|
380
|
+
assert isinstance(variable, Variable), f"The variable must be an instance of Variable, not {type(variable)}."
|
381
|
+
assert isinstance(criteria, Criteria), f"The criteria must be an instance of Criteria, not {type(criteria)}."
|
382
|
+
self._variable = None
|
383
|
+
self.variable = variable
|
384
|
+
self._criteria = None
|
385
|
+
self.criteria = criteria
|
386
|
+
self._score: np.ma.MaskedArray = None
|
387
|
+
self._dtype = None
|
388
|
+
self.dtype = dtype
|
389
|
+
|
390
|
+
@property
|
391
|
+
def variable(self) -> Variable:
|
392
|
+
"""
|
393
|
+
Get the variable.
|
394
|
+
|
395
|
+
:return: The variable as a Variable object.
|
396
|
+
"""
|
397
|
+
return self._variable
|
398
|
+
|
399
|
+
@variable.setter
|
400
|
+
def variable(self, value: Variable) -> None:
|
401
|
+
"""
|
402
|
+
Set the variable.
|
403
|
+
|
404
|
+
:param value: The variable to set.
|
405
|
+
"""
|
406
|
+
assert isinstance(value, Variable), f"The variable must be an instance of Variable, not {type(value)}."
|
407
|
+
self._variable = value
|
408
|
+
|
409
|
+
@property
|
410
|
+
def criteria(self) -> Criteria:
|
411
|
+
"""
|
412
|
+
Get the criteria.
|
413
|
+
|
414
|
+
:return: The criteria as a Criteria object.
|
415
|
+
"""
|
416
|
+
return self._criteria
|
417
|
+
|
418
|
+
@criteria.setter
|
419
|
+
def criteria(self, value: Criteria) -> None:
|
420
|
+
"""
|
421
|
+
Set the criteria.
|
422
|
+
|
423
|
+
:param value: The criteria to set.
|
424
|
+
"""
|
425
|
+
assert isinstance(value, Criteria), f"The criteria must be an instance of Criteria, not {type(value)}."
|
426
|
+
self._criteria = value
|
427
|
+
|
428
|
+
@property
|
429
|
+
def score(self) -> np.ma.MaskedArray:
|
430
|
+
"""
|
431
|
+
Get the score.
|
432
|
+
|
433
|
+
:return: The score as a numpy masked array.
|
434
|
+
"""
|
435
|
+
self.score = self._compute_score()
|
436
|
+
return self._score
|
437
|
+
|
438
|
+
@score.setter
|
439
|
+
def score(self, value: np.ma.MaskedArray) -> None:
|
440
|
+
"""
|
441
|
+
Set the score.
|
442
|
+
|
443
|
+
:param value: The score to set.
|
444
|
+
"""
|
445
|
+
assert isinstance(value, np.ma.MaskedArray), f"The score must be a numpy masked array, not {type(value)}."
|
446
|
+
assert value.shape == self.variable.variable.shape, \
|
447
|
+
f"The score must have the same shape as the variable, not {value.shape}."
|
448
|
+
self._score = value.astype(self._dtype, copy=True)
|
449
|
+
|
450
|
+
@property
|
451
|
+
def dtype(self) -> type:
|
452
|
+
"""
|
453
|
+
Get the data type of the score.
|
454
|
+
|
455
|
+
:return: The data type of the score.
|
456
|
+
"""
|
457
|
+
return self._dtype
|
458
|
+
|
459
|
+
@dtype.setter
|
460
|
+
def dtype(self, value: Union[type, str]) -> None:
|
461
|
+
"""
|
462
|
+
Set the data type of the score.
|
463
|
+
|
464
|
+
:param value: The data type to set.
|
465
|
+
"""
|
466
|
+
assert isinstance(value, (type, str)), f"The data type must be a numpy dtype or a string representing a numpy dtype, not {type(value)}."
|
467
|
+
assert np.issubdtype(value, np.integer), \
|
468
|
+
f"The data type must be an integer type, not {value}."
|
469
|
+
self._dtype = value
|
470
|
+
if self._score is not None:
|
471
|
+
self._score = self._score.astype(value, copy=True)
|
472
|
+
|
473
|
+
def _compute_score(self):
|
474
|
+
"""
|
475
|
+
Compute the score based on the variable and criteria.
|
476
|
+
|
477
|
+
:return: The score as a numpy masked array.
|
478
|
+
"""
|
479
|
+
assert self._variable is not None, "Variable is not set."
|
480
|
+
assert self._criteria is not None, "Criteria is not set."
|
481
|
+
assert isinstance(self._variable.variable, np.ma.MaskedArray), \
|
482
|
+
f"The variable must be a numpy masked array, not {type(self._variable.variable)}"
|
483
|
+
score:np.ma.MaskedArray = self.variable.variable.copy()
|
484
|
+
score = score.astype(Format.INT32.value, copy=True) # Convert to int32 for scoring
|
485
|
+
|
486
|
+
if self.criteria.condition == Operator.SUPERIOR.value:
|
487
|
+
score[self.variable.variable <= self.criteria.threshold] = Constant.ZERO.value
|
488
|
+
score[self.variable.variable > self.criteria.threshold] = Constant.ONE.value
|
489
|
+
|
490
|
+
elif self.criteria.condition == Operator.INFERIOR.value:
|
491
|
+
score[self.variable.variable >= self.criteria.threshold] = Constant.ZERO.value
|
492
|
+
score[self.variable.variable < self.criteria.threshold] = Constant.ONE.value
|
493
|
+
|
494
|
+
elif self.criteria.condition == Operator.EQUAL.value:
|
495
|
+
score[self.variable.variable != self.criteria.threshold] = Constant.ZERO.value
|
496
|
+
score[self.variable.variable == self.criteria.threshold] = Constant.ONE.value
|
497
|
+
|
498
|
+
elif self.criteria.condition == Operator.SUPERIOR_OR_EQUAL.value:
|
499
|
+
score[self.variable.variable < self.criteria.threshold] = Constant.ZERO.value
|
500
|
+
score[self.variable.variable >= self.criteria.threshold] = Constant.ONE.value
|
501
|
+
|
502
|
+
elif self.criteria.condition == Operator.INFERIOR_OR_EQUAL.value:
|
503
|
+
score[self.variable.variable > self.criteria.threshold] = Constant.ZERO.value
|
504
|
+
score[self.variable.variable <= self.criteria.threshold] = Constant.ONE.value
|
505
|
+
|
506
|
+
elif self.criteria.condition == Operator.BETWEEN.value:
|
507
|
+
assert isinstance(self.criteria.threshold, tuple), \
|
508
|
+
"The threshold for the 'between' condition must be a tuple of two elements."
|
509
|
+
score[(self.variable.variable < self.criteria.threshold[0]) | \
|
510
|
+
(self.variable.variable > self.criteria.threshold[1])] = Constant.ZERO.value
|
511
|
+
score[(self.variable.variable >= self.criteria.threshold[0]) & \
|
512
|
+
(self.variable.variable <= self.criteria.threshold[1])] = Constant.ONE.value
|
513
|
+
|
514
|
+
elif self.criteria.condition == Operator.BETWEEN_STRICT.value:
|
515
|
+
assert isinstance(self.criteria.threshold, tuple), \
|
516
|
+
"The threshold for the 'strictly between' condition must be a tuple of two elements."
|
517
|
+
score[(self.variable.variable <= self.criteria.threshold[0]) | \
|
518
|
+
(self.variable.variable >= self.criteria.threshold[1])] = Constant.ZERO.value
|
519
|
+
score[(self.variable.variable > self.criteria.threshold[0]) & \
|
520
|
+
(self.variable.variable < self.criteria.threshold[1])] = Constant.ONE.value
|
521
|
+
|
522
|
+
elif self.criteria.condition == Operator.OUTSIDE.value:
|
523
|
+
assert isinstance(self.criteria.threshold, tuple), \
|
524
|
+
"The threshold for the 'outside' condition must be a tuple of two elements."
|
525
|
+
score[(self.variable.variable > self.criteria.threshold[0]) & \
|
526
|
+
(self.variable.variable < self.criteria.threshold[1])] = Constant.ZERO.value
|
527
|
+
score[(self.variable.variable <= self.criteria.threshold[0]) | \
|
528
|
+
(self.variable.variable >= self.criteria.threshold[1])] = Constant.ONE.value
|
529
|
+
|
530
|
+
elif self.criteria.condition == Operator.OUTSIDE_STRICT.value:
|
531
|
+
assert isinstance(self.criteria.threshold, tuple), \
|
532
|
+
"The threshold for the 'strictly outside' condition must be a tuple of two elements."
|
533
|
+
score[(self.variable.variable >= self.criteria.threshold[0]) | \
|
534
|
+
(self.variable.variable <= self.criteria.threshold[1])] = Constant.ZERO.value
|
535
|
+
score[(self.variable.variable < self.criteria.threshold[0]) | \
|
536
|
+
(self.variable.variable > self.criteria.threshold[1])] = Constant.ONE.value
|
537
|
+
|
538
|
+
else:
|
539
|
+
raise ValueError(f"Unknown condition: {self._criteria.condition}. "
|
540
|
+
f"Available conditions are: {self._criteria._available_conditions}")
|
541
|
+
|
542
|
+
return score.astype(self.dtype, copy=True)
|
543
|
+
|
544
|
+
class Scores:
|
545
|
+
"""
|
546
|
+
This class is used to store multiple scores from different criteria as dictionary.
|
547
|
+
|
548
|
+
It contains a dictionary of scores,
|
549
|
+
where the keys are the score names and the values are the Score objects.
|
550
|
+
"""
|
551
|
+
def __init__(self, scores:dict[str, Score]) -> None:
|
552
|
+
"""
|
553
|
+
Initialize the Scores object.
|
554
|
+
"""
|
555
|
+
self._scores: dict[str, Score] = None
|
556
|
+
self.scores = scores
|
557
|
+
|
558
|
+
@property
|
559
|
+
def scores(self) -> dict[str, Score]:
|
560
|
+
"""
|
561
|
+
Get the scores.
|
562
|
+
|
563
|
+
:return: The scores as a dictionary of Score objects.
|
564
|
+
"""
|
565
|
+
return self._scores
|
566
|
+
|
567
|
+
@scores.setter
|
568
|
+
def scores(self, value: dict[str, Score]) -> None:
|
569
|
+
"""
|
570
|
+
Set the scores.
|
571
|
+
|
572
|
+
:param value: The scores to set.
|
573
|
+
"""
|
574
|
+
assert isinstance(value, dict), f"The scores must be a dictionary, not {type(value)}."
|
575
|
+
assert all(isinstance(key, str) for key in value.keys()), \
|
576
|
+
"All keys in the scores dictionary must be strings."
|
577
|
+
assert all(isinstance(score, Score) for score in value.values()), \
|
578
|
+
"All values in the scores dictionary must be Score objects."
|
579
|
+
assert all(score.variable.variable.shape == next(iter(value.values())).variable.variable.shape \
|
580
|
+
for score in value.values()), "All scores must have the same shape."
|
581
|
+
self._scores = value
|
582
|
+
|
583
|
+
@property
|
584
|
+
def number(self) -> int:
|
585
|
+
"""
|
586
|
+
Get the number of scores.
|
587
|
+
|
588
|
+
:return: The number of scores in the dictionary.
|
589
|
+
"""
|
590
|
+
return len(self._scores) if self._scores is not None else 0
|
591
|
+
|
592
|
+
def add_score(self, name: str, score: Score) -> None:
|
593
|
+
"""
|
594
|
+
Add a score to the scores dictionary.
|
595
|
+
|
596
|
+
:param name: The name of the score.
|
597
|
+
:param score: The Score object to add.
|
598
|
+
"""
|
599
|
+
assert isinstance(name, str), f"The name must be a string, not {type(name)}."
|
600
|
+
assert isinstance(score, Score), f"The score must be an instance of Score, not {type(score)}."
|
601
|
+
if self._scores is None:
|
602
|
+
self._scores = {}
|
603
|
+
self._scores[name] = score
|
604
|
+
|
605
|
+
def get_score(self, name: str) -> Score:
|
606
|
+
"""
|
607
|
+
Get a score by its name.
|
608
|
+
|
609
|
+
:param name: The name of the score to get.
|
610
|
+
:return: The Score object with the given name.
|
611
|
+
"""
|
612
|
+
try:
|
613
|
+
return self._scores[name]
|
614
|
+
except KeyError:
|
615
|
+
logging.error(f"The score with name: '{name}' does not exist.")
|
616
|
+
raise KeyError(f"The score with name '{name}' does not exist.")
|
617
|
+
|
618
|
+
def remove_score(self, name: str) -> None:
|
619
|
+
"""
|
620
|
+
Remove a score by its name.
|
621
|
+
|
622
|
+
:param name: The name of the score to remove.
|
623
|
+
"""
|
624
|
+
try:
|
625
|
+
del self._scores[name]
|
626
|
+
except KeyError:
|
627
|
+
logging.error(f"The score with name:'{name}' does not exist.")
|
628
|
+
raise KeyError(f"The score with name '{name}' does not exist.")
|
629
|
+
|
630
|
+
class Operations:
|
631
|
+
"""
|
632
|
+
This class is used to perform mathematical operations on scores.
|
633
|
+
|
634
|
+
N.B.: if needed operations from other libraries can be added here.
|
635
|
+
for instance **pymcdm, scikit-mcda, etc.**
|
636
|
+
"""
|
637
|
+
|
638
|
+
def __init__(self,
|
639
|
+
scores: Scores,
|
640
|
+
int_type:str = Format.INT32.value,
|
641
|
+
float_type:str = Format.FLOAT32.value,
|
642
|
+
weight: dict = None
|
643
|
+
) -> None:
|
644
|
+
"""
|
645
|
+
Initialize the Operations object with a Scores object.
|
646
|
+
|
647
|
+
:param scores: The Scores object to perform operations on.
|
648
|
+
:type scores: Scores
|
649
|
+
:param int_type: The integer data type to use in the operations if integers are expected,
|
650
|
+
defaults to np.int32.
|
651
|
+
:type int_type: str
|
652
|
+
:param float_type: The float data type to use in the operations if floats are expected
|
653
|
+
, defaults to np.float32.
|
654
|
+
:type float_type: str
|
655
|
+
:param weight: A dictionary of weights for scores, defaults to None.
|
656
|
+
:type weight: dict
|
657
|
+
|
658
|
+
:raises AssertionError: If the scores is not an instance of Scores.
|
659
|
+
"""
|
660
|
+
assert isinstance(scores, Scores),\
|
661
|
+
f"The scores must be an instance of Scores, not {type(scores)}."
|
662
|
+
self._scores = None
|
663
|
+
self._int_type = None
|
664
|
+
self._float_type = None
|
665
|
+
self._weight = None
|
666
|
+
self.scores = scores
|
667
|
+
self.int_type = int_type
|
668
|
+
self.float_type = float_type
|
669
|
+
self.weight = weight
|
670
|
+
self._available_operations = [Operator.SUM.value,
|
671
|
+
Operator.PRODUCT.value,
|
672
|
+
Operator.WEIGHTED_SUM.value,
|
673
|
+
Operator.AVERAGE.value]
|
674
|
+
|
675
|
+
@property
|
676
|
+
def scores(self) -> Scores:
|
677
|
+
"""
|
678
|
+
Get the Scores object.
|
679
|
+
|
680
|
+
:return: The Scores object.
|
681
|
+
"""
|
682
|
+
return self._scores
|
683
|
+
|
684
|
+
@scores.setter
|
685
|
+
def scores(self, value: Scores) -> None:
|
686
|
+
"""
|
687
|
+
Set the Scores object.
|
688
|
+
|
689
|
+
:param value: The Scores object to set.
|
690
|
+
"""
|
691
|
+
assert isinstance(value, Scores),\
|
692
|
+
f"The scores must be an instance of Scores, not {type(value)}."
|
693
|
+
self._scores = value
|
694
|
+
|
695
|
+
@property
|
696
|
+
def int_type(self) -> str:
|
697
|
+
"""
|
698
|
+
Get the integer data type used in the operations.
|
699
|
+
|
700
|
+
:return: The integer data type as a string.
|
701
|
+
"""
|
702
|
+
return self._int_type
|
703
|
+
@int_type.setter
|
704
|
+
def int_type(self, value: Union[str,type]) -> None:
|
705
|
+
"""
|
706
|
+
Set the integer data type used in the operations.
|
707
|
+
|
708
|
+
:param value: The integer data type to set.
|
709
|
+
"""
|
710
|
+
assert isinstance(value, (str,type)), f"The integer type must be a string, not {type(value)}."
|
711
|
+
self._int_type = value
|
712
|
+
|
713
|
+
@property
|
714
|
+
def float_type(self) -> str:
|
715
|
+
"""
|
716
|
+
Get the float data type used in the operations.
|
717
|
+
|
718
|
+
:return: The float data type as a string.
|
719
|
+
"""
|
720
|
+
return self._float_type
|
721
|
+
|
722
|
+
@float_type.setter
|
723
|
+
def float_type(self, value: Union[str,type]) -> None:
|
724
|
+
"""
|
725
|
+
Set the float data type used in the operations.
|
726
|
+
|
727
|
+
:param value: The float data type to set.
|
728
|
+
"""
|
729
|
+
assert isinstance(value, (str,type)), f"The float type must be a string, not {type(value)}."
|
730
|
+
self._float_type = value
|
731
|
+
|
732
|
+
@property
|
733
|
+
def weight(self) -> dict:
|
734
|
+
"""
|
735
|
+
Get the weight dictionary.
|
736
|
+
|
737
|
+
:return: The weight dictionary.
|
738
|
+
"""
|
739
|
+
return self._weight
|
740
|
+
|
741
|
+
@weight.setter
|
742
|
+
def weight(self, value: dict) -> None:
|
743
|
+
"""
|
744
|
+
Set the weight dictionary.
|
745
|
+
|
746
|
+
:param value: The weight dictionary to set.
|
747
|
+
"""
|
748
|
+
if value is None:
|
749
|
+
if self.scores.number == 0:
|
750
|
+
logging.warning("No scores to set weight for. Setting weight to None.")
|
751
|
+
self._weight = None
|
752
|
+
return
|
753
|
+
elif self.scores.number > 0:
|
754
|
+
logging.warning("No weight provided. Setting weight to equal distribution.")
|
755
|
+
value = {key: 1 for key in self.scores.scores.keys()}
|
756
|
+
else:
|
757
|
+
assert isinstance(value, dict), f"The weight must be a dictionary, not {type(value)}."
|
758
|
+
assert len(value) == self.scores.number,\
|
759
|
+
"The weight dictionary must have the same number of keys as the scores dictionary."
|
760
|
+
assert all(key in self.scores.scores.keys() for key in value.keys()),\
|
761
|
+
"All keys in the weight dictionary must be present in the scores dictionary."
|
762
|
+
self._weight = value
|
763
|
+
|
764
|
+
def sum(self) -> np.ma.MaskedArray:
|
765
|
+
"""
|
766
|
+
Sum all scores in the Scores object.
|
767
|
+
|
768
|
+
:return: The sum of all scores as a numpy masked array.
|
769
|
+
"""
|
770
|
+
if self.scores.number == 0:
|
771
|
+
logging.warning("No scores to sum.")
|
772
|
+
return np.ma.masked_array([], mask=True)
|
773
|
+
score_sum = np.ma.masked_array(np.zeros_like(next(iter(self.scores.scores.values())).score),
|
774
|
+
dtype=Format.INT32.value)
|
775
|
+
|
776
|
+
for score in self.scores.scores.values():
|
777
|
+
score_sum += score.score
|
778
|
+
|
779
|
+
return score_sum
|
780
|
+
|
781
|
+
def product(self) -> np.ma.MaskedArray:
|
782
|
+
"""
|
783
|
+
Calculate the product of all scores in the Scores object.
|
784
|
+
|
785
|
+
:return: The product of all scores as a numpy masked array.
|
786
|
+
"""
|
787
|
+
if self.scores.number == 0:
|
788
|
+
logging.warning("No scores to multiply.")
|
789
|
+
return np.ma.masked_array([], mask=True)
|
790
|
+
|
791
|
+
score_product = np.ma.masked_array(np.ones_like(next(iter(self.scores.scores.values())).score),
|
792
|
+
dtype=Format.INT32.value)
|
793
|
+
|
794
|
+
for score in self.scores.scores.values():
|
795
|
+
score_product *= score.score
|
796
|
+
|
797
|
+
return score_product
|
798
|
+
|
799
|
+
|
800
|
+
def weighted_sum(self) -> np.ma.MaskedArray:
|
801
|
+
"""
|
802
|
+
Calculate the weighted sum of all scores in the Scores object.
|
803
|
+
|
804
|
+
:return: The weighted sum of all scores as a numpy masked array.
|
805
|
+
"""
|
806
|
+
if self.scores.number == 0:
|
807
|
+
logging.warning("No scores to calculate weighted sum.")
|
808
|
+
return np.ma.masked_array([], mask=True)
|
809
|
+
|
810
|
+
score_sum = np.ma.masked_array(np.zeros_like(next(iter(self.scores.scores.values())).score),
|
811
|
+
dtype=self.float_type) # FIXME Find a better method to apply this
|
812
|
+
|
813
|
+
for key, score in self.scores.scores.items():
|
814
|
+
if key in self.weight:
|
815
|
+
array = (score.score * self.weight[key]).astype(self.float_type, copy=True)
|
816
|
+
score_sum = score_sum.astype(self.float_type, copy=True)
|
817
|
+
score_sum += array
|
818
|
+
|
819
|
+
else:
|
820
|
+
raise KeyError(f"No weight provided for score '{key}'. Using default weight of 1.")
|
821
|
+
|
822
|
+
return score_sum
|
823
|
+
|
824
|
+
def average(self) -> np.ma.MaskedArray:
|
825
|
+
"""
|
826
|
+
Calculate the average of all scores in the Scores object.
|
827
|
+
|
828
|
+
:return: The average of all scores as a numpy masked array.
|
829
|
+
"""
|
830
|
+
if self.scores.number == 0:
|
831
|
+
logging.warning("No scores to average.")
|
832
|
+
return np.ma.masked_array([], mask=True)
|
833
|
+
|
834
|
+
score_sum = self.sum()
|
835
|
+
average: np.ma.MaskedArray = (score_sum / self.scores.number)
|
836
|
+
average = average.astype(self.float_type)
|
837
|
+
|
838
|
+
return average
|
839
|
+
|
840
|
+
def percentage(self) -> np.ma.MaskedArray:
|
841
|
+
"""
|
842
|
+
Calculate the percentage of each score in the Scores object.
|
843
|
+
|
844
|
+
:return: The percentage of each score as a numpy masked array.
|
845
|
+
"""
|
846
|
+
return self.average() * 100
|
847
|
+
|
848
|
+
def apply_operation(self,
|
849
|
+
operation: Literal[Operator.SUM.value,
|
850
|
+
Operator.PRODUCT.value,
|
851
|
+
Operator.WEIGHTED_SUM.value,
|
852
|
+
Operator.AVERAGE.value,
|
853
|
+
Operator.PERCENTAGE.value
|
854
|
+
]) -> np.ma.MaskedArray:
|
855
|
+
"""
|
856
|
+
Select an operation to perform on the scores.
|
857
|
+
|
858
|
+
:param operation: The operation to perform.
|
859
|
+
:return: The result of the operation as a numpy masked array.
|
860
|
+
"""
|
861
|
+
# if operation.lower() not in self._available_operations():
|
862
|
+
if operation == Operator.SUM.value:
|
863
|
+
return self.sum()
|
864
|
+
elif operation == Operator.PRODUCT.value:
|
865
|
+
return self.product()
|
866
|
+
elif operation == Operator.WEIGHTED_SUM.value:
|
867
|
+
return self.weighted_sum()
|
868
|
+
elif operation == Operator.AVERAGE.value:
|
869
|
+
return self.average()
|
870
|
+
elif operation == Operator.PERCENTAGE.value:
|
871
|
+
return self.percentage()
|
872
|
+
else:
|
873
|
+
raise ValueError(f"Unknown operation: {operation}. "
|
874
|
+
f"Available operations are: {self._available_operations()}")
|
875
|
+
|
876
|
+
class Results:
|
877
|
+
"""
|
878
|
+
This class is used to collect results of the Operations module.
|
879
|
+
|
880
|
+
It allows:
|
881
|
+
- to define the mold (a WolfArray) which serve as the geo-spatial extent computed results,
|
882
|
+
- to define the method used to compute scores,
|
883
|
+
- to get and write the results.
|
884
|
+
"""
|
885
|
+
def __init__(self,
|
886
|
+
operations: Operations,
|
887
|
+
mold: WolfArray = None,
|
888
|
+
method: Literal[Operator.SUM.value,
|
889
|
+
Operator.PRODUCT.value,
|
890
|
+
Operator.WEIGHTED_SUM.value,
|
891
|
+
Operator.AVERAGE.value,
|
892
|
+
Operator.PERCENTAGE.value
|
893
|
+
] = Operator.SUM.value
|
894
|
+
) -> None:
|
895
|
+
|
896
|
+
"""
|
897
|
+
Initialize the Results object.
|
898
|
+
|
899
|
+
:param operations: The Operations object to perform operations on.
|
900
|
+
:type operations: Operations
|
901
|
+
:param mold: The mold used for the results, defaults to None.
|
902
|
+
:type mold: WolfArray
|
903
|
+
:param method: The method used to compute the scores, defaults sum.
|
904
|
+
:type method: str,
|
905
|
+
|
906
|
+
"""
|
907
|
+
self._operations = None
|
908
|
+
self._mold = None
|
909
|
+
self._method = None
|
910
|
+
self.operations = operations
|
911
|
+
self.mold = mold
|
912
|
+
self.method = method
|
913
|
+
|
914
|
+
@property
|
915
|
+
def operations(self) -> Operations:
|
916
|
+
"""
|
917
|
+
Get the Operations object.
|
918
|
+
|
919
|
+
:return: The Operations object.
|
920
|
+
"""
|
921
|
+
return self._operations
|
922
|
+
|
923
|
+
@operations.setter
|
924
|
+
def operations(self, value: Operations) -> None:
|
925
|
+
"""
|
926
|
+
Set the Operations object.
|
927
|
+
|
928
|
+
:param value: The Operations object to set.
|
929
|
+
"""
|
930
|
+
assert isinstance(value, Operations),\
|
931
|
+
f"The operations must be an instance of Operations, not {type(value)}."
|
932
|
+
self._operations = value
|
933
|
+
|
934
|
+
@property
|
935
|
+
def mold(self) -> WolfArray:
|
936
|
+
"""
|
937
|
+
Get the mold used for the results.
|
938
|
+
|
939
|
+
:return: The mold as a WolfArray.
|
940
|
+
"""
|
941
|
+
|
942
|
+
return self._mold
|
943
|
+
|
944
|
+
@mold.setter
|
945
|
+
def mold(self, value: WolfArray) -> None:
|
946
|
+
"""
|
947
|
+
Set the mold used for the results.
|
948
|
+
:param value: The mold to set.
|
949
|
+
"""
|
950
|
+
if value is not None:
|
951
|
+
assert isinstance(value, WolfArray), f"The mold must be a WolfArray, not {type(value)}."
|
952
|
+
for key in self.operations.scores.scores.keys():
|
953
|
+
if value.shape != self.operations.scores.get_score(key).score.shape:
|
954
|
+
raise ValueError(f"The mold shape {value.shape} does not match the score shape {self.operations.scores.get_score(key).score.shape}.")
|
955
|
+
self._mold = value
|
956
|
+
|
957
|
+
@property
|
958
|
+
def header(self) -> header_wolf:
|
959
|
+
"""
|
960
|
+
Get the header of the mold.
|
961
|
+
|
962
|
+
:return: The header of the mold as a header_wolf object.
|
963
|
+
"""
|
964
|
+
if self.mold is None:
|
965
|
+
if self.operations is not None:
|
966
|
+
sample = self.operations.scores.get_score(next(iter(self.operations.scores.scores.keys()))).score
|
967
|
+
nbx = sample.shape[0]
|
968
|
+
nby = sample.shape[1]
|
969
|
+
return self.create_header_wolf(nbx=nbx,
|
970
|
+
nby=nby,
|
971
|
+
dx= Constant.ONE.value,
|
972
|
+
dy= Constant.ONE.value)
|
973
|
+
else:
|
974
|
+
return self.mold.get_header()
|
975
|
+
|
976
|
+
@property
|
977
|
+
def method(self) -> str:
|
978
|
+
"""
|
979
|
+
Get the method used for the results.
|
980
|
+
|
981
|
+
:return: The method as a string.
|
982
|
+
"""
|
983
|
+
return self._method
|
984
|
+
|
985
|
+
@method.setter
|
986
|
+
def method(self, value: Literal[Operator.SUM.value,
|
987
|
+
Operator.PRODUCT.value,
|
988
|
+
Operator.WEIGHTED_SUM.value,
|
989
|
+
Operator.AVERAGE.value,
|
990
|
+
Operator.PERCENTAGE.value
|
991
|
+
]) -> None:
|
992
|
+
"""
|
993
|
+
Set the method used for the results.
|
994
|
+
:param value: The method to set.
|
995
|
+
|
996
|
+
"""
|
997
|
+
assert value in self.operations._available_operations,\
|
998
|
+
f"The method must be one of the following: {self.operations._available_operations}."
|
999
|
+
self._method = value
|
1000
|
+
|
1001
|
+
@property
|
1002
|
+
def as_numpy_array(self) -> np.ma.MaskedArray:
|
1003
|
+
return self.operations.apply_operation(self.method)
|
1004
|
+
|
1005
|
+
@property
|
1006
|
+
def as_WolfArray(self) -> WolfArray:
|
1007
|
+
"""
|
1008
|
+
Get the results as a WolfArray.
|
1009
|
+
|
1010
|
+
:return: The results as a WolfArray.
|
1011
|
+
"""
|
1012
|
+
return self.as_results(self.as_numpy_array)
|
1013
|
+
|
1014
|
+
#------------
|
1015
|
+
# Methods
|
1016
|
+
#------------
|
1017
|
+
|
1018
|
+
def create_header_wolf(self,
|
1019
|
+
origx:float = None,
|
1020
|
+
origy: float =None ,
|
1021
|
+
origz: float = None,
|
1022
|
+
dx: float = None,
|
1023
|
+
dy: float = None,
|
1024
|
+
dz: float = None,
|
1025
|
+
nbx: int = None,
|
1026
|
+
nby: int = None,
|
1027
|
+
nbz: int = None):
|
1028
|
+
"""
|
1029
|
+
Create a header_wolf object.
|
1030
|
+
The header_wolf object is used to describe the spatial
|
1031
|
+
characteristics of the array.
|
1032
|
+
|
1033
|
+
|
1034
|
+
:param origx: The x-coordinate of the origin (in 2D - lower left corner),
|
1035
|
+
defaults to None.
|
1036
|
+
:type origx: float, optional
|
1037
|
+
:param origy: The y-coordinate of the origin (in 2D - lower left corner),
|
1038
|
+
defaults to None.
|
1039
|
+
:type origy: float, optional
|
1040
|
+
:param origz: The z-coordinate of the origin, defaults to None.
|
1041
|
+
:type origz: float, optional
|
1042
|
+
:param dx: The x-spacing (discretization in the x direction), defaults to None.
|
1043
|
+
:type dx: float, optional
|
1044
|
+
:param dy: The y-spacing (discretization in the x direction), defaults to None.
|
1045
|
+
:type dy: float, optional
|
1046
|
+
:param dz: The z-spacing, defaults to None.
|
1047
|
+
:type dz: float, optional
|
1048
|
+
:param nbx: The number of columns, defaults to None.
|
1049
|
+
:type nbx: int, optional
|
1050
|
+
:param nby: The number of rows, defaults to None.
|
1051
|
+
:type nby: int, optional
|
1052
|
+
:param nbz: The number of layers, defaults to None.
|
1053
|
+
:type nbz: int, optional
|
1054
|
+
:return: A header_wolf object with the given parameters.
|
1055
|
+
:rtype: header_wolf
|
1056
|
+
"""
|
1057
|
+
header = header_wolf()
|
1058
|
+
if origx is not None:
|
1059
|
+
header.origx = origx
|
1060
|
+
if origy is not None:
|
1061
|
+
header.origy = origy
|
1062
|
+
if origz is not None:
|
1063
|
+
header.origz = origz
|
1064
|
+
if dx is not None:
|
1065
|
+
header.dx = dx
|
1066
|
+
if dy is not None:
|
1067
|
+
header.dy = dy
|
1068
|
+
if dz is not None:
|
1069
|
+
header.dz = dz
|
1070
|
+
if nbx is not None:
|
1071
|
+
header.nbx = nbx
|
1072
|
+
if nby is not None:
|
1073
|
+
header.nby = nby
|
1074
|
+
if nbz is not None:
|
1075
|
+
header.nbz = nbz
|
1076
|
+
return header
|
1077
|
+
|
1078
|
+
def create_WolfArray(self,
|
1079
|
+
dtype = None) -> WolfArray:
|
1080
|
+
"""
|
1081
|
+
Create an empty WolfArray with the given name, and header.
|
1082
|
+
|
1083
|
+
:param dtype: The data type of the WolfArray, defaults to np.float32.
|
1084
|
+
:type dtype: Union[type, str]
|
1085
|
+
:return: An empty WolfArray with the given name and header.
|
1086
|
+
:rtype: WolfArray
|
1087
|
+
"""
|
1088
|
+
array = WolfArray()
|
1089
|
+
array.init_from_header(self.header, dtype=dtype)
|
1090
|
+
return array
|
1091
|
+
|
1092
|
+
def as_results(self,
|
1093
|
+
results: np.ma.MaskedArray,
|
1094
|
+
EPSG: int = 31370,
|
1095
|
+
dtype: Union[type, str] =None,
|
1096
|
+
write_to: Union [Path,str] = None
|
1097
|
+
) -> WolfArray:
|
1098
|
+
"""
|
1099
|
+
Write the results to a WolfArray file.
|
1100
|
+
|
1101
|
+
:param output_path: The path to write the results to.
|
1102
|
+
:param name: The name of the results file.
|
1103
|
+
:param header: The header for the WolfArray.
|
1104
|
+
:param dtype: The data type of the WolfArray.
|
1105
|
+
"""
|
1106
|
+
if dtype is None:
|
1107
|
+
dtype = results.dtype
|
1108
|
+
array = self.create_WolfArray(dtype=dtype)
|
1109
|
+
array.array = results
|
1110
|
+
array.array.mask = results.mask
|
1111
|
+
array.nullvalue = -99999
|
1112
|
+
if write_to is not None:
|
1113
|
+
assert isinstance(write_to, (str, Path)), \
|
1114
|
+
f"The output path must be a string or a Path object, not {type(write_to)}."
|
1115
|
+
# if not os.path.exists(write_to):
|
1116
|
+
# os.makedirs(write_to)
|
1117
|
+
array.write_all(write_to, EPSG=EPSG)
|
1118
|
+
return array
|
1119
|
+
|
1120
|
+
class Input:
|
1121
|
+
"""
|
1122
|
+
This class stores the inputs of the MultiCriteriaAnalysis.
|
1123
|
+
|
1124
|
+
Also, it ensures that the inputs are valid and consistent
|
1125
|
+
to feed the MulticriteriAnalysis tool.
|
1126
|
+
"""
|
1127
|
+
def __init__(self,
|
1128
|
+
name: str,
|
1129
|
+
array: np.ma.MaskedArray| np.ndarray| WolfArray,
|
1130
|
+
condition: Literal[Operator.SUPERIOR.value,
|
1131
|
+
Operator.INFERIOR.value,
|
1132
|
+
Operator.EQUAL.value,
|
1133
|
+
Operator.SUPERIOR_OR_EQUAL.value,
|
1134
|
+
Operator.INFERIOR_OR_EQUAL.value] = Operator.SUPERIOR_OR_EQUAL.value,
|
1135
|
+
threshold: Union[float, int] = 0.0,
|
1136
|
+
) -> None:
|
1137
|
+
"""
|
1138
|
+
Initialize the Input object.
|
1139
|
+
|
1140
|
+
:param name: The name of the variable.
|
1141
|
+
:type name: str
|
1142
|
+
:param array: The array (matrix) containing the variable (observations).
|
1143
|
+
:type array: np.ma.MaskedArray, np.ndarray, or WolfArray
|
1144
|
+
:param condition: The condition (criteria) used to discriminate values inside the variable,
|
1145
|
+
defaults to 'superior or equal'.
|
1146
|
+
:type condition: str, optional
|
1147
|
+
:param threshold: The threshold used to select values inside the variable,
|
1148
|
+
defaults to 0.0.
|
1149
|
+
:type threshold: Union[float, int], optional
|
1150
|
+
"""
|
1151
|
+
self._name = None
|
1152
|
+
self._array = None
|
1153
|
+
self._condition = None
|
1154
|
+
self._threshold = None
|
1155
|
+
self._score = None
|
1156
|
+
self.name = name
|
1157
|
+
self.array = array
|
1158
|
+
self.condition = condition
|
1159
|
+
self.threshold = threshold
|
1160
|
+
|
1161
|
+
@property
|
1162
|
+
def name(self) -> str:
|
1163
|
+
"""
|
1164
|
+
Get the name of the input.
|
1165
|
+
|
1166
|
+
:return: The name of the input.
|
1167
|
+
"""
|
1168
|
+
return self._name
|
1169
|
+
|
1170
|
+
@name.setter
|
1171
|
+
def name(self, value: str) -> None:
|
1172
|
+
"""
|
1173
|
+
Set the name of the input.
|
1174
|
+
|
1175
|
+
:param value: The name to set.
|
1176
|
+
"""
|
1177
|
+
assert isinstance(value, str), f"The name must be a string, not {type(value)}."
|
1178
|
+
self._name = value
|
1179
|
+
|
1180
|
+
@property
|
1181
|
+
def array(self) -> Union[np.ma.MaskedArray, np.ndarray, WolfArray]:
|
1182
|
+
"""
|
1183
|
+
Get the array of the input.
|
1184
|
+
|
1185
|
+
:return: The array as a numpy masked array.
|
1186
|
+
"""
|
1187
|
+
return self._array
|
1188
|
+
|
1189
|
+
@array.setter
|
1190
|
+
def array(self, value: Union[np.ma.MaskedArray, np.ndarray, WolfArray
|
1191
|
+
]) -> None:
|
1192
|
+
"""
|
1193
|
+
Set the array of the input.
|
1194
|
+
|
1195
|
+
:param value: The array to set.
|
1196
|
+
"""
|
1197
|
+
assert isinstance(value, (np.ma.MaskedArray, np.ndarray, WolfArray)), \
|
1198
|
+
f"The array must be a numpy masked array, a numpy array or a WolfArray, not {type(value)}."
|
1199
|
+
self._array = value
|
1200
|
+
|
1201
|
+
@property
|
1202
|
+
def condition(self) -> Literal[Operator.SUPERIOR.value,
|
1203
|
+
Operator.INFERIOR.value,
|
1204
|
+
Operator.EQUAL.value,
|
1205
|
+
Operator.SUPERIOR_OR_EQUAL.value,
|
1206
|
+
Operator.INFERIOR_OR_EQUAL.value]:
|
1207
|
+
"""
|
1208
|
+
Get the condition of the input.
|
1209
|
+
|
1210
|
+
:return: The condition as a string.
|
1211
|
+
"""
|
1212
|
+
return self._condition
|
1213
|
+
|
1214
|
+
@condition.setter
|
1215
|
+
def condition(self, value: Literal[Operator.SUPERIOR.value,
|
1216
|
+
Operator.INFERIOR.value,
|
1217
|
+
Operator.EQUAL.value,
|
1218
|
+
Operator.SUPERIOR_OR_EQUAL.value,
|
1219
|
+
Operator.INFERIOR_OR_EQUAL.value]) -> None:
|
1220
|
+
"""
|
1221
|
+
Set the condition of the input.
|
1222
|
+
|
1223
|
+
:param value: The condition to set.
|
1224
|
+
"""
|
1225
|
+
assert isinstance(value, str), f"The condition must be a string, not {type(value)}."
|
1226
|
+
self._condition = value
|
1227
|
+
|
1228
|
+
@property
|
1229
|
+
def threshold(self) -> Union[float, int]:
|
1230
|
+
"""
|
1231
|
+
Get the threshold of the input.
|
1232
|
+
|
1233
|
+
:return: The threshold as a float or an int.
|
1234
|
+
"""
|
1235
|
+
return self._threshold
|
1236
|
+
|
1237
|
+
@threshold.setter
|
1238
|
+
def threshold(self, value: Union[float, int, tuple, list, np.ndarray]) -> None:
|
1239
|
+
"""
|
1240
|
+
Set the threshold of the input.
|
1241
|
+
|
1242
|
+
:param value: The threshold to set.
|
1243
|
+
FIXME make it fair for list as well.
|
1244
|
+
"""
|
1245
|
+
assert isinstance(value, (float, int, tuple, list, np.ndarray)),\
|
1246
|
+
f"The threshold must be a float or an int or a tuple, not {type(value)}."
|
1247
|
+
if isinstance(value, (tuple, list)):
|
1248
|
+
assert len(value) == 2, "The threshold must be a tuple or a list with exactly two elements."
|
1249
|
+
assert all(isinstance(item, (float, int)) for item in value), \
|
1250
|
+
f"All elements in the threshold tuple or list must be floats or ints.\
|
1251
|
+
not {[type(item) for item in value]}."
|
1252
|
+
value = tuple(value) # Ensure it's a tuple
|
1253
|
+
elif isinstance(value, np.ndarray):
|
1254
|
+
assert value.ndim == 1, "The threshold must be a 1D numpy"
|
1255
|
+
assert len(value) == 2, "The threshold must be a 1D numpy array with exactly two elements."
|
1256
|
+
# val1 = value[0]
|
1257
|
+
if np.issubdtype(value.dtype, np.integer):
|
1258
|
+
value = (int(value[0]), int(value[1]))
|
1259
|
+
elif np.issubdtype(value.dtype,np.floating):
|
1260
|
+
value = (float(value[0]), float(value[1]))
|
1261
|
+
# value = (float(value[0]), float(value[1])) # Convert to tuple
|
1262
|
+
self._threshold = value
|
1263
|
+
|
1264
|
+
@property
|
1265
|
+
def score(self) -> Score:
|
1266
|
+
"""
|
1267
|
+
Get the score of the input.
|
1268
|
+
|
1269
|
+
:return: The score as a Score object.
|
1270
|
+
"""
|
1271
|
+
if self._score is None:
|
1272
|
+
self._score = Score(variable= Variable(variable = self.array),
|
1273
|
+
criteria= Criteria(threshold=self.threshold, condition=self.condition, ))
|
1274
|
+
return self._score
|
1275
|
+
|
1276
|
+
|
1277
|
+
@score.setter
|
1278
|
+
def score(self, value: Score) -> None:
|
1279
|
+
"""
|
1280
|
+
Set the score of the input.
|
1281
|
+
|
1282
|
+
:param value: The score to set.
|
1283
|
+
"""
|
1284
|
+
assert isinstance(value, Score), f"The score must be an instance of Score, not {type(value)}."
|
1285
|
+
self._score = value
|
1286
|
+
|
1287
|
+
class MulticriteriAnalysis:
|
1288
|
+
"""
|
1289
|
+
This class performs a multi-criteria analysis based
|
1290
|
+
on a set of inputs (variables and criteria).
|
1291
|
+
|
1292
|
+
The inputs are:
|
1293
|
+
- name: The name of the variable (observations).
|
1294
|
+
- array: The array (matrix) containing the variable (observations).
|
1295
|
+
- condition: The condition (criteria) used to distinguish between good and bad variables.
|
1296
|
+
Basically, the condition is used to obtain a binary score (0 or 1) for each value in the array.
|
1297
|
+
- Threshold: The value(s) used as limits for the condition.
|
1298
|
+
"""
|
1299
|
+
def __init__(self,
|
1300
|
+
inputs: Union[list[Input],tuple[Input],Input],
|
1301
|
+
method:Literal[Operator.SUM.value,
|
1302
|
+
Operator.AVERAGE.value,
|
1303
|
+
Operator.PERCENTAGE.value] = Operator.PERCENTAGE.value,
|
1304
|
+
dtype: Union[type, str] = Format.FLOAT32.value,
|
1305
|
+
mold: WolfArray = None,
|
1306
|
+
write_to: Union[Path, str] = None,
|
1307
|
+
EPSG: int = None
|
1308
|
+
) -> None:
|
1309
|
+
"""
|
1310
|
+
Initialize the MultiCriteriaAnalysis
|
1311
|
+
|
1312
|
+
:param inputs: The inputs to the MultiCriteriaAnalysis.
|
1313
|
+
:type inputs: Union[list[Input], tuple[Input], Input]
|
1314
|
+
:param method: The method used for the MultiCriteriaAnalysis,
|
1315
|
+
defaults to 'percentage'.
|
1316
|
+
:type method: Literal[constant_value(Constant.SUM),
|
1317
|
+
constant_value(Constant.AVERAGE),
|
1318
|
+
constant_value(Constant.PERCENTAGE)], optional
|
1319
|
+
:param dtype: The data type used for the MultiCriteriaAnalysis,
|
1320
|
+
defaults to np.float32.
|
1321
|
+
:type dtype: Union[type, str], optional
|
1322
|
+
:param mold: The mold used for the MultiCriteriaAnalysis,
|
1323
|
+
defaults to None.
|
1324
|
+
:type mold: WolfArray, optional
|
1325
|
+
:param write_to: The path where the results will be written,
|
1326
|
+
defaults to None.
|
1327
|
+
:type write_to: Union[Path, str], optional
|
1328
|
+
:param EPSG: The EPSG code for the coordinate reference system,
|
1329
|
+
defaults to None.
|
1330
|
+
:type EPSG: int, optional
|
1331
|
+
"""
|
1332
|
+
self._inputs: list[Input] = None
|
1333
|
+
self._method: str = None
|
1334
|
+
self._dtype: Union[type, str] = None
|
1335
|
+
self._mold: WolfArray = None
|
1336
|
+
self._path: Union[Path, str] = None
|
1337
|
+
self._EPSG: int = None
|
1338
|
+
self.inputs = inputs
|
1339
|
+
self.method = method
|
1340
|
+
self.dtype = dtype
|
1341
|
+
self.mold = mold
|
1342
|
+
self.path = write_to
|
1343
|
+
self.EPSG = EPSG
|
1344
|
+
|
1345
|
+
# ************************
|
1346
|
+
# Preprocessing the inputs
|
1347
|
+
# ************************
|
1348
|
+
|
1349
|
+
@property
|
1350
|
+
def inputs(self) -> list[Input]:
|
1351
|
+
"""
|
1352
|
+
Get the inputs of the MultiCriteriaAnalysis.
|
1353
|
+
|
1354
|
+
:return: The inputs as a list of Input objects.
|
1355
|
+
"""
|
1356
|
+
return self._inputs
|
1357
|
+
|
1358
|
+
@inputs.setter
|
1359
|
+
def inputs(self, value: Union[list[Input],tuple[Input],Input]) -> None:
|
1360
|
+
"""
|
1361
|
+
Set the inputs of the MultiCriteriaAnalysis.
|
1362
|
+
|
1363
|
+
:param value: The inputs to set.
|
1364
|
+
"""
|
1365
|
+
assert isinstance(value, (list, tuple, Input)), \
|
1366
|
+
f"The inputs must be a list or a tuple of Input objects, not {type(value)}."
|
1367
|
+
if isinstance(value, (list, tuple)):
|
1368
|
+
assert len(value) > 0, "The inputs list must not be empty."
|
1369
|
+
assert all(isinstance(item, Input) for item in value), \
|
1370
|
+
"All items in the inputs list must be Input objects."
|
1371
|
+
if isinstance(value, tuple):
|
1372
|
+
value = list(value)
|
1373
|
+
elif isinstance(value, Input):
|
1374
|
+
value = [value]
|
1375
|
+
|
1376
|
+
self._inputs = value
|
1377
|
+
|
1378
|
+
@property
|
1379
|
+
def number_of_inputs(self) -> int:
|
1380
|
+
"""
|
1381
|
+
Get the number of inputs.
|
1382
|
+
|
1383
|
+
:return: The number of inputs.
|
1384
|
+
"""
|
1385
|
+
return len(self.inputs) if self.inputs is not None else 0
|
1386
|
+
|
1387
|
+
@property
|
1388
|
+
def method(self) -> Literal[Operator.SUM.value,
|
1389
|
+
Operator.AVERAGE.value,
|
1390
|
+
Operator.PERCENTAGE.value]:
|
1391
|
+
"""
|
1392
|
+
Get the method used for the MultiCriteriaAnalysis.
|
1393
|
+
|
1394
|
+
:return: The method as a string.
|
1395
|
+
"""
|
1396
|
+
return self._method
|
1397
|
+
|
1398
|
+
@method.setter
|
1399
|
+
def method(self, value: Literal[Operator.SUM.value,
|
1400
|
+
Operator.AVERAGE.value,
|
1401
|
+
Operator.PERCENTAGE.value]) -> None:
|
1402
|
+
"""
|
1403
|
+
Set the method used for the MultiCriteriaAnalysis.
|
1404
|
+
|
1405
|
+
:param value: The method to set.
|
1406
|
+
"""
|
1407
|
+
assert isinstance(value, str), f"The method must be a string, not {type(value)}."
|
1408
|
+
self._method = value
|
1409
|
+
|
1410
|
+
@property
|
1411
|
+
def dtype(self) -> Union[type, str]:
|
1412
|
+
"""
|
1413
|
+
Get the data type used for the MultiCriteriaAnalysis.
|
1414
|
+
|
1415
|
+
:return: The data type as a string or a numpy dtype.
|
1416
|
+
"""
|
1417
|
+
return self._dtype
|
1418
|
+
|
1419
|
+
@dtype.setter
|
1420
|
+
def dtype(self, value: Union[type, str]) -> None:
|
1421
|
+
"""
|
1422
|
+
Set the data type used for the MultiCriteriaAnalysis.
|
1423
|
+
|
1424
|
+
:param value: The data type to set.
|
1425
|
+
"""
|
1426
|
+
if value is None:
|
1427
|
+
value = Format.FLOAT32.value # Default data type
|
1428
|
+
else:
|
1429
|
+
assert isinstance(value, (type, str)),\
|
1430
|
+
f"The data type must be a numpy dtype or a string representing a numpy dtype, not {type(value)}."
|
1431
|
+
self._dtype = value
|
1432
|
+
|
1433
|
+
@property
|
1434
|
+
def mold(self) -> WolfArray:
|
1435
|
+
"""
|
1436
|
+
Get the mold used for the MultiCriteriaAnalysis.
|
1437
|
+
|
1438
|
+
:return: The mold as a WolfArray.
|
1439
|
+
"""
|
1440
|
+
return self._mold
|
1441
|
+
|
1442
|
+
@mold.setter
|
1443
|
+
def mold(self, value: WolfArray) -> None:
|
1444
|
+
"""
|
1445
|
+
Set the mold used for the MultiCriteriaAnalysis.
|
1446
|
+
|
1447
|
+
:param value: The mold to set.
|
1448
|
+
|
1449
|
+
FIXME: add update mold from inputs.
|
1450
|
+
"""
|
1451
|
+
if value is not None:
|
1452
|
+
assert isinstance(value, WolfArray), f"The mold must be a WolfArray, not {type(value)}."
|
1453
|
+
for input_ in self.inputs:
|
1454
|
+
if value.shape != input_.array.shape:
|
1455
|
+
raise ValueError(f"The mold shape {value.shape} does not match the input shape {input_.array.shape}.")
|
1456
|
+
self._mold = value
|
1457
|
+
|
1458
|
+
@property
|
1459
|
+
def path(self) -> Union[Path, str]:
|
1460
|
+
"""
|
1461
|
+
Get the path where the results will be written.
|
1462
|
+
|
1463
|
+
:return: The path as a string or a Path object.
|
1464
|
+
"""
|
1465
|
+
return self._path
|
1466
|
+
|
1467
|
+
@path.setter
|
1468
|
+
def path(self, value: Union[Path, str]) -> None:
|
1469
|
+
"""
|
1470
|
+
Set the path where the results will be written.
|
1471
|
+
|
1472
|
+
:param value: The path to set.
|
1473
|
+
"""
|
1474
|
+
if value is None:
|
1475
|
+
self._path = None
|
1476
|
+
else:
|
1477
|
+
assert isinstance(value, (Path, str)),\
|
1478
|
+
f"The path must be a string or a Path object, not {type(value)}."
|
1479
|
+
self._path = value
|
1480
|
+
|
1481
|
+
@property
|
1482
|
+
def scores(self) -> Scores:
|
1483
|
+
"""
|
1484
|
+
Get the scores for each input.
|
1485
|
+
|
1486
|
+
:return: The scores as a Scores object.
|
1487
|
+
"""
|
1488
|
+
scores = {}
|
1489
|
+
for input_ in self.inputs:
|
1490
|
+
scores[input_.name] = input_.score
|
1491
|
+
|
1492
|
+
return Scores(scores)
|
1493
|
+
|
1494
|
+
@property
|
1495
|
+
def operations(self) -> Operations:
|
1496
|
+
"""
|
1497
|
+
Get the operations to be performed on the scores.
|
1498
|
+
|
1499
|
+
:return: The Operations object.
|
1500
|
+
"""
|
1501
|
+
return Operations(scores=self.scores, int_type=self.dtype, float_type=self.dtype)
|
1502
|
+
|
1503
|
+
@property
|
1504
|
+
def EPSG(self) -> int:
|
1505
|
+
"""
|
1506
|
+
Get the EPSG code for the results.
|
1507
|
+
|
1508
|
+
:return: The EPSG code as an integer.
|
1509
|
+
"""
|
1510
|
+
return self._EPSG
|
1511
|
+
|
1512
|
+
@EPSG.setter
|
1513
|
+
def EPSG(self, value: int) -> None:
|
1514
|
+
"""
|
1515
|
+
Set the EPSG code for the results.
|
1516
|
+
|
1517
|
+
:param value: The EPSG code to set.
|
1518
|
+
"""
|
1519
|
+
if value is None:
|
1520
|
+
self._EPSG = Constant.EPSG.value # Default EPSG code
|
1521
|
+
else:
|
1522
|
+
assert isinstance(value, int), f"The EPSG code must be an integer, not {type(value)}."
|
1523
|
+
self._EPSG = value
|
1524
|
+
|
1525
|
+
# *************************************
|
1526
|
+
# Performing the MultiCriteria Analysis
|
1527
|
+
# *************************************
|
1528
|
+
|
1529
|
+
@property
|
1530
|
+
def results(self) -> WolfArray:
|
1531
|
+
"""
|
1532
|
+
Perform the MultiCriteria Analysis on the inputs.
|
1533
|
+
|
1534
|
+
:return: The results of the analysis as a Results object.
|
1535
|
+
"""
|
1536
|
+
if self.number_of_inputs == 0:
|
1537
|
+
raise ValueError("No inputs provided for the MultiCriteria Analysis.")
|
1538
|
+
assert self.number_of_inputs >= 2, "At least two inputs are required for the MultiCriteria Analysis."
|
1539
|
+
assert self.method in self.operations._available_operations,\
|
1540
|
+
f"The method '{self.method}' is not available.\
|
1541
|
+
Available methods are: {self.operations._available_operations}"
|
1542
|
+
# Create results object
|
1543
|
+
results = Results(operations=self.operations, mold=self.mold, method=self.method)
|
1544
|
+
return results.as_WolfArray
|
1545
|
+
|
1546
|
+
def write_result(self) -> WolfArray:
|
1547
|
+
"""
|
1548
|
+
Write the results to a WolfArray file.
|
1549
|
+
|
1550
|
+
:return: The results as a WolfArray.
|
1551
|
+
"""
|
1552
|
+
if self.path is None:
|
1553
|
+
raise ValueError("No path provided for writing the results.")
|
1554
|
+
return self.results.write_all(self.path, self.EPSG)
|
1555
|
+
|
1556
|
+
|
1557
|
+
|
1558
|
+
|
1559
|
+
|
1560
|
+
|
1561
|
+
|
1562
|
+
|
1563
|
+
|
1564
|
+
|
1565
|
+
|
1566
|
+
|
1567
|
+
|
1568
|
+
|
1569
|
+
|
1570
|
+
|
1571
|
+
|
1572
|
+
|
1573
|
+
|
1574
|
+
|
1575
|
+
|
1576
|
+
|
1577
|
+
|
1578
|
+
|
1579
|
+
|