pysits 1.5.3.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- LICENSE +339 -0
- README.md +83 -0
- pysits/__init__.py +243 -0
- pysits/backend/__init__.py +18 -0
- pysits/backend/functions.py +50 -0
- pysits/backend/loaders.py +92 -0
- pysits/backend/pkgs.py +35 -0
- pysits/conversions/__init__.py +18 -0
- pysits/conversions/clojure.py +68 -0
- pysits/conversions/common.py +280 -0
- pysits/conversions/decorators.py +148 -0
- pysits/conversions/dsl/__init__.py +18 -0
- pysits/conversions/dsl/base.py +43 -0
- pysits/conversions/dsl/mask.py +337 -0
- pysits/conversions/dsl/tuning.py +129 -0
- pysits/conversions/tibble.py +444 -0
- pysits/conversions/tibble_arrow.py +469 -0
- pysits/conversions/vector.py +137 -0
- pysits/conversions/xarray.py +207 -0
- pysits/docs/__init__.py +22 -0
- pysits/docs/content/sits_get_data.md +85 -0
- pysits/docs/decorators.py +58 -0
- pysits/extras/__init__.py +82 -0
- pysits/extras/earthdatalogin.py +44 -0
- pysits/extras/torch.py +44 -0
- pysits/jinja.py +56 -0
- pysits/models/__init__.py +18 -0
- pysits/models/base.py +34 -0
- pysits/models/data/__init__.py +22 -0
- pysits/models/data/accuracy.py +94 -0
- pysits/models/data/base.py +51 -0
- pysits/models/data/cube.py +210 -0
- pysits/models/data/frame.py +138 -0
- pysits/models/data/frame_accessor.py +133 -0
- pysits/models/data/matrix.py +119 -0
- pysits/models/data/table.py +49 -0
- pysits/models/data/ts.py +161 -0
- pysits/models/data/tuning.py +307 -0
- pysits/models/data/vector.py +47 -0
- pysits/models/frame.py +191 -0
- pysits/models/ml.py +27 -0
- pysits/models/resolver.py +147 -0
- pysits/settings.py +44 -0
- pysits/sits/__init__.py +18 -0
- pysits/sits/classification.py +42 -0
- pysits/sits/colors.py +50 -0
- pysits/sits/config.py +45 -0
- pysits/sits/context.py +36 -0
- pysits/sits/cube.py +214 -0
- pysits/sits/data.py +220 -0
- pysits/sits/exporters/__init__.py +29 -0
- pysits/sits/exporters/files.py +37 -0
- pysits/sits/exporters/sf.py +30 -0
- pysits/sits/exporters/xarray.py +65 -0
- pysits/sits/impute.py +38 -0
- pysits/sits/ml.py +110 -0
- pysits/sits/segment.py +38 -0
- pysits/sits/tiles.py +35 -0
- pysits/sits/ts.py +216 -0
- pysits/sits/tuning.py +82 -0
- pysits/sits/utils.py +126 -0
- pysits/sits/visualization.py +122 -0
- pysits/templates/cube.html +196 -0
- pysits/templates/tuning.html +183 -0
- pysits/visualization/__init__.py +24 -0
- pysits/visualization/base.py +163 -0
- pysits/visualization/image.py +48 -0
- pysits/visualization/leaflet.py +94 -0
- pysits/visualization/tmap.py +85 -0
- pysits-1.5.3.dev0.dist-info/METADATA +123 -0
- pysits-1.5.3.dev0.dist-info/RECORD +73 -0
- pysits-1.5.3.dev0.dist-info/WHEEL +4 -0
- pysits-1.5.3.dev0.dist-info/licenses/LICENSE +339 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""backend functions."""
|
|
19
|
+
|
|
20
|
+
from pysits.backend.loaders import load_function_from_package
|
|
21
|
+
|
|
22
|
+
# Base - plot
|
|
23
|
+
r_fnc_plot = load_function_from_package("base::plot")
|
|
24
|
+
|
|
25
|
+
# Base - set column
|
|
26
|
+
r_fnc_set_column = load_function_from_package("base::$<-")
|
|
27
|
+
|
|
28
|
+
# Base - summary (base)
|
|
29
|
+
r_fnc_summary = load_function_from_package("base::summary")
|
|
30
|
+
|
|
31
|
+
# Base - readRDS (base)
|
|
32
|
+
r_fnc_read_rds = load_function_from_package("base::readRDS")
|
|
33
|
+
|
|
34
|
+
# Base - system.file (base)
|
|
35
|
+
r_fnc_system_file = load_function_from_package("base::system_file")
|
|
36
|
+
|
|
37
|
+
# Base - set.seed (base)
|
|
38
|
+
r_fnc_set_seed = load_function_from_package("base::set_seed")
|
|
39
|
+
|
|
40
|
+
# Base - class (base)
|
|
41
|
+
r_fnc_class = load_function_from_package("base::class")
|
|
42
|
+
|
|
43
|
+
# Base - as.data.frame (base)
|
|
44
|
+
r_fnc_as_data_frame = load_function_from_package("base::as_data_frame")
|
|
45
|
+
|
|
46
|
+
# Base - colnames (base)
|
|
47
|
+
r_fnc_colnames = load_function_from_package("base::colnames")
|
|
48
|
+
|
|
49
|
+
# Base - rownames (base)
|
|
50
|
+
r_fnc_rownames = load_function_from_package("base::rownames")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""backend loaders."""
|
|
19
|
+
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from rpy2.robjects import r
|
|
24
|
+
from rpy2.robjects.packages import importr
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_data_from_package(name: str, package: str, **kwargs) -> object:
|
|
28
|
+
"""Load data from package.
|
|
29
|
+
|
|
30
|
+
This function loads data from a package. It uses `data` behind the scenes.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
name (str): Dataset name.
|
|
34
|
+
|
|
35
|
+
package (str): Package name.
|
|
36
|
+
|
|
37
|
+
**kwargs: Additional arguments to pass to the function.
|
|
38
|
+
"""
|
|
39
|
+
return r.data(name, package=package, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_data_from_global(name: str) -> object:
|
|
43
|
+
"""Load data from global environment.
|
|
44
|
+
|
|
45
|
+
This function loads data from the global environment.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name (str): Dataset name.
|
|
49
|
+
"""
|
|
50
|
+
return r[name]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_function_from_package(name: str) -> Callable[..., Any]:
|
|
54
|
+
"""Load an R function from a specified package.
|
|
55
|
+
|
|
56
|
+
This function takes a fully qualified R function name in the format
|
|
57
|
+
'package::function' and returns a callable Python wrapper for that R function.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
name (str): The fully qualified name of the R function in the format
|
|
61
|
+
'package::function'. For example, 'stats::median' or 'base::mean'.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Callable[..., Any]: A Python callable that wraps the R function.
|
|
65
|
+
The exact signature depends on the underlying R function.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If the ``name`` doesn't follow the 'package::function' format.
|
|
69
|
+
|
|
70
|
+
PackageNotFoundError: If the specified R package is not installed.
|
|
71
|
+
|
|
72
|
+
AttributeError: If the function doesn't exist in the specified package.
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
>>> median_func = load_function_from_package('stats::median')
|
|
76
|
+
>>> result = median_func([1, 2, 3, 4, 5])
|
|
77
|
+
"""
|
|
78
|
+
# Parse package and function
|
|
79
|
+
try:
|
|
80
|
+
package_name, func_name = name.split("::")
|
|
81
|
+
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Invalid function name format: {name}. "
|
|
85
|
+
"Expected format: 'package::function'"
|
|
86
|
+
) from e
|
|
87
|
+
|
|
88
|
+
# Import the package
|
|
89
|
+
pkg = importr(package_name, on_conflict="warn")
|
|
90
|
+
|
|
91
|
+
# Return function
|
|
92
|
+
return getattr(pkg, func_name)
|
pysits/backend/pkgs.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""backend packages."""
|
|
19
|
+
|
|
20
|
+
from rpy2.robjects.packages import importr
|
|
21
|
+
|
|
22
|
+
# system pakage
|
|
23
|
+
r_pkg_base = importr("base")
|
|
24
|
+
r_pkg_grdevices = importr("grDevices")
|
|
25
|
+
|
|
26
|
+
# sits package
|
|
27
|
+
r_pkg_sits = importr("sits")
|
|
28
|
+
|
|
29
|
+
# sits-dependencies packages
|
|
30
|
+
r_pkg_tibble = importr("tibble")
|
|
31
|
+
r_pkg_leaflet = importr("leaflet")
|
|
32
|
+
r_pkg_kohonen = importr("kohonen")
|
|
33
|
+
r_pkg_sf = importr("sf")
|
|
34
|
+
r_pkg_htmlwidgets = importr("htmlwidgets")
|
|
35
|
+
r_pkg_arrow = importr("arrow")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""conversions module."""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""Clojure utilities."""
|
|
19
|
+
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any, ParamSpec, TypeVar
|
|
22
|
+
|
|
23
|
+
from pysits.backend.pkgs import r_pkg_sits
|
|
24
|
+
from pysits.conversions.decorators import rpy2_fix_type, rpy2_fix_type_custom
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Generics
|
|
28
|
+
#
|
|
29
|
+
R = TypeVar("R")
|
|
30
|
+
P = ParamSpec("P")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# Factory function
|
|
35
|
+
#
|
|
36
|
+
def closure_factory(
|
|
37
|
+
name: str, converters: dict[str, Callable[[Any], Any]] = {}
|
|
38
|
+
) -> Callable[P, R]:
|
|
39
|
+
"""Factory to create sits-based closure functions.
|
|
40
|
+
|
|
41
|
+
This function creates a closure that wraps R package sits functions,
|
|
42
|
+
making them callable from Python. It checks if the requested function
|
|
43
|
+
exists in the R sits package before creating the wrapper.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name (str): Name of the sits-based function to be wrapped from the R package.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Callable[P, R]: A wrapped function that forwards calls to the R sits package.
|
|
50
|
+
The function accepts any arguments (*args, **kwargs) and returns the result
|
|
51
|
+
from the R function.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the specified function name does not exist in the R sits package.
|
|
55
|
+
"""
|
|
56
|
+
if not hasattr(r_pkg_sits, name):
|
|
57
|
+
raise ValueError(f"Invalid function: {name}")
|
|
58
|
+
|
|
59
|
+
# define method closure
|
|
60
|
+
@rpy2_fix_type_custom(converters)
|
|
61
|
+
@rpy2_fix_type
|
|
62
|
+
def _fnc(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
63
|
+
return getattr(r_pkg_sits, name)(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
# set function name
|
|
66
|
+
_fnc.__name__ = name
|
|
67
|
+
|
|
68
|
+
return _fnc
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""Common conversions."""
|
|
19
|
+
|
|
20
|
+
from datetime import date, timedelta
|
|
21
|
+
from pathlib import Path, PosixPath
|
|
22
|
+
|
|
23
|
+
import rpy2.robjects as ro
|
|
24
|
+
from geopandas import GeoDataFrame as GeoPandasDataFrame
|
|
25
|
+
from pandas import DataFrame as PandasDataFrame
|
|
26
|
+
from rpy2.rinterface_lib.sexp import NULLType
|
|
27
|
+
from rpy2.robjects import pandas2ri
|
|
28
|
+
from rpy2.robjects.robject import RObjectMixin
|
|
29
|
+
|
|
30
|
+
from pysits.backend.pkgs import r_pkg_tibble
|
|
31
|
+
from pysits.conversions.dsl.base import DSLObject
|
|
32
|
+
from pysits.conversions.tibble import geopandas_to_tibble, pandas_to_tibble
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Type mapping dictionary
|
|
36
|
+
#
|
|
37
|
+
TYPE_CONVERSIONS = {
|
|
38
|
+
list: lambda obj: convert_list_like_to_r(obj),
|
|
39
|
+
tuple: lambda obj: convert_list_like_to_r(obj),
|
|
40
|
+
set: lambda obj: convert_list_like_to_r(list(obj)),
|
|
41
|
+
dict: lambda obj: convert_dict_like_to_r(obj),
|
|
42
|
+
bool: lambda obj: ro.BoolVector([obj]),
|
|
43
|
+
int: lambda obj: ro.IntVector([obj]),
|
|
44
|
+
float: lambda obj: ro.FloatVector([obj]),
|
|
45
|
+
str: lambda obj: ro.StrVector([obj]),
|
|
46
|
+
Path: lambda obj: ro.StrVector([obj.as_posix()]),
|
|
47
|
+
PosixPath: lambda obj: ro.StrVector([obj.as_posix()]),
|
|
48
|
+
PandasDataFrame: lambda obj: r_pkg_tibble.as_tibble(pandas_to_tibble(obj)),
|
|
49
|
+
GeoPandasDataFrame: lambda obj: geopandas_to_tibble(obj),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# R epoch reference
|
|
55
|
+
#
|
|
56
|
+
EPOCH_START = date(1970, 1, 1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# Base utilities
|
|
61
|
+
#
|
|
62
|
+
def _is_numeric(x: int | float) -> bool:
|
|
63
|
+
"""Helper function to check if a value is numeric (int or float)."""
|
|
64
|
+
return isinstance(x, int | float)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
# Base conversions
|
|
69
|
+
#
|
|
70
|
+
def convert_list_like_to_r(obj: list | tuple | set) -> ro.vectors.Vector: # noqa: PLR0911
|
|
71
|
+
"""
|
|
72
|
+
Converts a list-like object to the appropriate R vector type.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
obj (list, tuple, set): The list-like Python object.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
An R-compatible vector.
|
|
79
|
+
"""
|
|
80
|
+
if not obj:
|
|
81
|
+
return ro.vectors.ListVector({})
|
|
82
|
+
|
|
83
|
+
if all(isinstance(x, int) for x in obj):
|
|
84
|
+
return ro.vectors.IntVector(obj)
|
|
85
|
+
|
|
86
|
+
elif all(isinstance(x, float) for x in obj):
|
|
87
|
+
return ro.vectors.FloatVector(obj)
|
|
88
|
+
|
|
89
|
+
# Check if all elements are numeric (int or float)
|
|
90
|
+
elif all(_is_numeric(x) for x in obj):
|
|
91
|
+
return ro.vectors.FloatVector(obj)
|
|
92
|
+
|
|
93
|
+
elif all(isinstance(x, str) for x in obj):
|
|
94
|
+
return ro.vectors.StrVector(obj)
|
|
95
|
+
|
|
96
|
+
elif all(isinstance(x, bool) for x in obj):
|
|
97
|
+
return ro.BoolVector(obj)
|
|
98
|
+
|
|
99
|
+
else:
|
|
100
|
+
return ro.vectors.ListVector(
|
|
101
|
+
{str(i): convert_to_r(v) for i, v in enumerate(obj)}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def convert_dict_like_as_list_to_r(obj: dict) -> ro.vectors.ListVector:
|
|
106
|
+
"""Convert a Python dict to a ListVector."""
|
|
107
|
+
return ro.vectors.ListVector({str(k): convert_to_r(v) for k, v in obj.items()})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def convert_dict_like_to_r(obj: dict) -> ro.vectors.Vector:
|
|
111
|
+
"""Convert a Python dictionary to an R vector.
|
|
112
|
+
|
|
113
|
+
This function converts a Python dictionary to either a typed R vector or ListVector,
|
|
114
|
+
depending on the types of values in the dictionary:
|
|
115
|
+
- If all values are of the same type (e.g. all strings, all numeric), returns an
|
|
116
|
+
appropriate typed R vector with named elements
|
|
117
|
+
- Otherwise, returns an R ListVector with converted values
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
obj (dict): A Python dictionary to convert to an R vector.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
ro.vectors.Vector: Either a typed R vector (if all values are of same type) or
|
|
124
|
+
an R ListVector (for mixed value types). The resulting vector will
|
|
125
|
+
preserve the dictionary's keys as names in the R vector.
|
|
126
|
+
"""
|
|
127
|
+
values = list(obj.values())
|
|
128
|
+
if not values:
|
|
129
|
+
return ro.vectors.ListVector({})
|
|
130
|
+
|
|
131
|
+
# Get data properties
|
|
132
|
+
has_unique_type = len(set(type(v) for v in values)) == 1
|
|
133
|
+
is_numeric = all(_is_numeric(v) for v in values)
|
|
134
|
+
|
|
135
|
+
# Handle homogeneous data
|
|
136
|
+
if has_unique_type or is_numeric:
|
|
137
|
+
vec = convert_list_like_to_r(values)
|
|
138
|
+
vec.names = list(obj.keys())
|
|
139
|
+
|
|
140
|
+
return vec
|
|
141
|
+
|
|
142
|
+
# For mixed data, use a list vector
|
|
143
|
+
return convert_dict_like_as_list_to_r(obj)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def convert_to_r(obj):
|
|
147
|
+
"""Convert Python objects to R-compatible objects for use with rpy2.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
obj: The Python object to convert.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
An R-compatible object.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
TypeError: If the object type cannot be converted.
|
|
157
|
+
"""
|
|
158
|
+
if obj is None:
|
|
159
|
+
return ro.r("NULL") # Convert None to R's NULL
|
|
160
|
+
|
|
161
|
+
obj_type = type(obj)
|
|
162
|
+
|
|
163
|
+
# Handle ``SITSBase`` objects
|
|
164
|
+
if getattr(obj, "_instance", None):
|
|
165
|
+
# Sync instance with R
|
|
166
|
+
if getattr(obj, "_sync_instance", None):
|
|
167
|
+
obj._sync_instance()
|
|
168
|
+
|
|
169
|
+
return obj._instance
|
|
170
|
+
|
|
171
|
+
# Handle ``raw R`` / Expressions objects
|
|
172
|
+
if isinstance(obj, RObjectMixin | DSLObject):
|
|
173
|
+
return obj
|
|
174
|
+
|
|
175
|
+
# Check if the object type exists in the conversion dictionary
|
|
176
|
+
if obj_type in TYPE_CONVERSIONS:
|
|
177
|
+
return TYPE_CONVERSIONS[obj_type](obj)
|
|
178
|
+
|
|
179
|
+
raise TypeError(f"Cannot convert object of type {obj_type} to R format")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def convert_to_python(obj, as_type="str"):
|
|
183
|
+
"""Convert an R object to a Python representation.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
obj (ro.Vector): The R object to convert.
|
|
187
|
+
|
|
188
|
+
as_type (str): The target Python type. Supported values:
|
|
189
|
+
- ``str``: Converts R string vector to Python list of strings;
|
|
190
|
+
|
|
191
|
+
- ``date``: Converts R DateVector to list of YYYY-MM-DD formatted strings;
|
|
192
|
+
|
|
193
|
+
- ``int``: Converts R numeric vector to Python list of integers;
|
|
194
|
+
|
|
195
|
+
- ``float``: Converts R numeric vector to Python list of floats;
|
|
196
|
+
|
|
197
|
+
- ``bool``: Converts R logical vector to Python list of booleans.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
list: Converted values as a Python list.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ValueError: If the specified ``as_type`` is not supported.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def _convert(value, type_):
|
|
207
|
+
result = []
|
|
208
|
+
|
|
209
|
+
if isinstance(value, ro.ListVector):
|
|
210
|
+
for k, v in value.items():
|
|
211
|
+
result.append(
|
|
212
|
+
{
|
|
213
|
+
str(k): convert_to_python(v, type_),
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
elif as_type and isinstance(value, ro.Vector):
|
|
218
|
+
for el in value:
|
|
219
|
+
if as_type == "str":
|
|
220
|
+
result.append(str(el))
|
|
221
|
+
|
|
222
|
+
elif as_type == "date":
|
|
223
|
+
result.append(
|
|
224
|
+
(EPOCH_START + timedelta(days=int(el))).strftime("%Y-%m-%d")
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
elif as_type == "int":
|
|
228
|
+
result.append(int(el))
|
|
229
|
+
|
|
230
|
+
elif as_type == "float":
|
|
231
|
+
result.append(float(el))
|
|
232
|
+
|
|
233
|
+
elif as_type == "bool":
|
|
234
|
+
result.append(bool(el))
|
|
235
|
+
|
|
236
|
+
elif isinstance(value, ro.Vector):
|
|
237
|
+
result = pandas2ri.rpy2py(value)
|
|
238
|
+
names = value.names
|
|
239
|
+
|
|
240
|
+
if not isinstance(names, NULLType):
|
|
241
|
+
result = {str(k): v for k, v in zip(names, result)}
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
return _convert(obj, as_type)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
#
|
|
249
|
+
# Parameters conversions
|
|
250
|
+
#
|
|
251
|
+
def fix_reserved_words_parameters(**kwargs) -> dict:
|
|
252
|
+
"""Fix reserved words parameters.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
**kwargs: The keyword arguments to fix.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
dict: The fixed keyword arguments.
|
|
259
|
+
"""
|
|
260
|
+
new_values = {}
|
|
261
|
+
keys_to_remove = []
|
|
262
|
+
|
|
263
|
+
for key, _ in kwargs.items():
|
|
264
|
+
# Assuming all reserved words end with "_"
|
|
265
|
+
if key.endswith("_"):
|
|
266
|
+
# Save key to remove
|
|
267
|
+
keys_to_remove.append(key)
|
|
268
|
+
|
|
269
|
+
# Save new value
|
|
270
|
+
new_values[key[:-1]] = kwargs[key]
|
|
271
|
+
|
|
272
|
+
# Remove keys that were converted
|
|
273
|
+
for key in keys_to_remove:
|
|
274
|
+
kwargs.pop(key)
|
|
275
|
+
|
|
276
|
+
# Add remaining kwargs
|
|
277
|
+
if new_values:
|
|
278
|
+
kwargs.update(new_values)
|
|
279
|
+
|
|
280
|
+
return kwargs
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""Decorators."""
|
|
19
|
+
|
|
20
|
+
import functools
|
|
21
|
+
import inspect
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from typing import Any, ParamSpec, TypeVar
|
|
24
|
+
|
|
25
|
+
from pysits.conversions.common import convert_to_r, fix_reserved_words_parameters
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# Generics
|
|
29
|
+
#
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
P = ParamSpec("P")
|
|
32
|
+
R = TypeVar("R")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# Decorator
|
|
37
|
+
#
|
|
38
|
+
def rpy2_fix_type(func: Callable[P, R]) -> Callable[P, R]:
|
|
39
|
+
"""Decorator function to convert arguments to R-compatible objects.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
func (Callable): The function whose arguments should be converted.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Callable: A wrapped function that receives converted arguments.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@functools.wraps(func)
|
|
49
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
50
|
+
kwargs = fix_reserved_words_parameters(**kwargs)
|
|
51
|
+
converted_args = [convert_to_r(arg) for arg in args]
|
|
52
|
+
converted_kwargs = {k: convert_to_r(v) for k, v in kwargs.items()}
|
|
53
|
+
return func(*converted_args, **converted_kwargs)
|
|
54
|
+
|
|
55
|
+
return wrapper
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def rpy2_fix_type_custom(
|
|
59
|
+
converters: dict[str, Callable[[Any], Any]],
|
|
60
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
61
|
+
"""Convert arguments to R objects using parameter-specific converters.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
converters (dict[str, Callable[[Any], Any]]): A dictionary mapping
|
|
65
|
+
parameter names to converter functions. Each converter function should
|
|
66
|
+
take a Python object and return an R-compatible object. Parameters not
|
|
67
|
+
found in this dictionary will be passed as is.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Callable: A decorator that wraps a function to convert its arguments using the
|
|
71
|
+
parameter-specific converters.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> converters = {
|
|
75
|
+
... "optimizer": convert_optimizer,
|
|
76
|
+
... "opt_hparams": convert_opt_hparams
|
|
77
|
+
... }
|
|
78
|
+
>>> @rpy2_fix_type_custom(converters)
|
|
79
|
+
... def my_function(optimizer, opt_hparams, other_arg):
|
|
80
|
+
... # optimizer and opt_hparams will use their specific converters
|
|
81
|
+
... # other_arg will use the default _convert_to_r
|
|
82
|
+
... pass
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
86
|
+
@functools.wraps(func)
|
|
87
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
88
|
+
# Get the function's parameter names
|
|
89
|
+
param_names = inspect.signature(func).parameters.keys()
|
|
90
|
+
param_names = list(param_names)
|
|
91
|
+
|
|
92
|
+
# Convert positional arguments using their parameter names
|
|
93
|
+
converted_args = []
|
|
94
|
+
for i, arg in enumerate(args):
|
|
95
|
+
if i < len(param_names) and param_names[i] in converters:
|
|
96
|
+
converted_args.append(converters[param_names[i]](arg))
|
|
97
|
+
else:
|
|
98
|
+
converted_args.append(arg)
|
|
99
|
+
|
|
100
|
+
# Convert keyword arguments
|
|
101
|
+
converted_kwargs = {}
|
|
102
|
+
for k, v in kwargs.items():
|
|
103
|
+
if k in converters:
|
|
104
|
+
converted_kwargs[k] = converters[k](v)
|
|
105
|
+
else:
|
|
106
|
+
converted_kwargs[k] = v
|
|
107
|
+
|
|
108
|
+
return func(*converted_args, **converted_kwargs)
|
|
109
|
+
|
|
110
|
+
return wrapper
|
|
111
|
+
|
|
112
|
+
return decorator
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def function_call(r_function: Callable[P, R], output_wrapper: Callable[[R], T]):
|
|
116
|
+
"""Decorator function to call an R function and post-process the result.
|
|
117
|
+
|
|
118
|
+
This decorator is used to wrap Python stub functions that serve as documentation
|
|
119
|
+
and type hint shells. The resulting function performs the following steps:
|
|
120
|
+
|
|
121
|
+
1. Converts all arguments to R-compatible types using `@rpy2_fix_type`.
|
|
122
|
+
2. Calls the provided R function (`r_function`) with converted arguments.
|
|
123
|
+
3. Wraps the result in a specified output Python class (`output_wrapper`).
|
|
124
|
+
|
|
125
|
+
This enables consistent logic while preserving per-function docstrings and
|
|
126
|
+
type hints for IDE support, autocompletion, and documentation generation.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
r_function (Callable): The R function to be called via rpy2.
|
|
130
|
+
|
|
131
|
+
output_wrapper (Callable): A callable (usually a class) that wraps the
|
|
132
|
+
result returned by the R function.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Callable: A decorator that wraps a Python function stub, providing the
|
|
136
|
+
R execution logic.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
140
|
+
@rpy2_fix_type
|
|
141
|
+
@functools.wraps(func)
|
|
142
|
+
def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
143
|
+
result = r_function(*args, **kwargs)
|
|
144
|
+
return output_wrapper(result)
|
|
145
|
+
|
|
146
|
+
return wrapped
|
|
147
|
+
|
|
148
|
+
return decorator
|