sgptools 1.2.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sgptools/utils/misc.py CHANGED
@@ -1,162 +1,196 @@
1
- from .metrics import get_distance
2
-
3
1
  from scipy.optimize import linear_sum_assignment
4
2
  from sklearn.metrics import pairwise_distances
5
3
  from scipy.cluster.vq import kmeans2
6
4
  from shapely import geometry
7
5
  import geopandas as gpd
8
6
 
9
- import matplotlib.pyplot as plt
10
7
  import numpy as np
8
+ from typing import Tuple, Optional, Union
11
9
 
12
10
 
13
- def get_inducing_pts(data, num_inducing, orientation=False, random=False):
14
- """Selects a subset of the data points to be used as inducing points.
15
- The default approach uses kmeans to select the subset.
11
+ def get_inducing_pts(data: np.ndarray,
12
+ num_inducing: int,
13
+ orientation: bool = False,
14
+ random: bool = False) -> np.ndarray:
15
+ """
16
+ Selects a subset of data points to be used as inducing points.
17
+ By default, it uses k-means clustering to select representative points.
18
+ Alternatively, it can select points randomly.
19
+ If `orientation` is True, an additional dimension representing a rotation angle
20
+ is appended to each inducing point.
16
21
 
17
22
  Args:
18
- data (ndarray): (n, 2); Data points to select the inducing points from
19
- num_inducing (int): Number of inducing points
20
- orientation (bool): If True, add an additional dimension to model the sensor
21
- FoV rotation angle
22
- random (bool): If True, the subset of inducing points are selected randomly
23
- instead of using kmeans
23
+ data (np.ndarray): (n, d_in); Input data points from which to select inducing points.
24
+ `n` is the number of data points, `d_in` is the input dimensionality.
25
+ num_inducing (int): The desired number of inducing points to select.
26
+ orientation (bool): If True, a random orientation angle (in radians, from 0 to 2*pi)
27
+ is added as an additional dimension to each inducing point.
28
+ Defaults to False.
29
+ random (bool): If True, inducing points are selected randomly from `data`.
30
+ If False, k-means clustering (`kmeans2`) is used for selection.
31
+ Defaults to False.
24
32
 
25
33
  Returns:
26
- Xu (ndarray): (m, d); Inducing points in the position and orientation space.
27
- `m` is the number of inducing points,
28
- `d` is the dimension of the space (x, y, optional - angle in radians)
34
+ np.ndarray: (m, d_out); Inducing points. `m` is `num_inducing`.
35
+ `d_out` is `d_in` if `orientation` is False, or `d_in + 1` if `orientation` is True.
36
+ If `orientation` is True, the last dimension contains angles in radians.
37
+
38
+ Usage:
39
+ ```python
40
+ import numpy as np
41
+ from sgptools.utils.misc import get_inducing_pts
42
+
43
+ # Example data (1000 2D points)
44
+ data_points = np.random.rand(1000, 2) * 100
45
+
46
+ # 1. Select 50 inducing points using k-means (default)
47
+ inducing_points_kmeans = get_inducing_pts(data_points, 50)
48
+
49
+ # 2. Select 20 inducing points randomly with orientation
50
+ inducing_points_random_oriented = get_inducing_pts(data_points, 20, orientation=True, random=True)
51
+ ```
29
52
  """
30
53
  if random:
31
- idx = np.random.randint(len(data), size=num_inducing)
54
+ # Randomly select `num_inducing` indices from the data
55
+ idx = np.random.choice(len(data), size=num_inducing, replace=False)
32
56
  Xu = data[idx]
33
57
  else:
58
+ # Use k-means clustering to find `num_inducing` cluster centers
59
+ # `minit="points"` initializes centroids by picking random data points
34
60
  Xu = kmeans2(data, num_inducing, minit="points")[0]
61
+
35
62
  if orientation:
63
+ # Generate random angles between 0 and 2*pi (radians)
36
64
  thetas = np.random.uniform(0, 2 * np.pi, size=(Xu.shape[0], 1))
65
+ # Concatenate the points with their corresponding angles
37
66
  Xu = np.concatenate([Xu, thetas], axis=1)
67
+
38
68
  return Xu
39
69
 
40
- def cont2disc(Xu, candidates, candidate_labels=None):
41
- """Map continuous space locations to a discrete set of candidate location
70
+
71
+ def cont2disc(
72
+ Xu: np.ndarray,
73
+ candidates: np.ndarray,
74
+ candidate_labels: Optional[np.ndarray] = None
75
+ ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
76
+ """
77
+ Maps continuous space locations (`Xu`) to the closest points in a discrete
78
+ set of candidate locations (`candidates`) using a Hungarian algorithm
79
+ (linear sum assignment) for optimal matching. This ensures each `Xu` point
80
+ is matched to a unique candidate.
42
81
 
43
82
  Args:
44
- Xu (ndarray): (m, 2); Continuous space points
45
- candidates (ndarray): (n, 2); Discrete set of candidate locations
46
- candidate_labels (ndarray): (n, 1); Labels corresponding to the discrete set of candidate locations
83
+ Xu (np.ndarray): (m, d); Continuous space points (e.g., optimized sensor locations).
84
+ `m` is the number of points, `d` is the dimensionality.
85
+ candidates (np.ndarray): (n, d); Discrete set of candidate locations.
86
+ `n` is the number of candidates, `d` is the dimensionality.
87
+ candidate_labels (Optional[np.ndarray]): (n, 1); Optional labels corresponding to
88
+ the discrete set of candidate locations.
89
+ If provided, the matched labels are also returned.
90
+ Defaults to None.
47
91
 
48
92
  Returns:
49
- Xu_x (ndarray): Discrete space points' locations
50
- Xu_y (ndarray): Labels of the discrete space points. Returned only if `candidate_labels`
51
- was passed to the function
52
-
93
+ Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
94
+ - If `candidate_labels` is None:
95
+ np.ndarray: (m, d); Discrete space points' locations (`Xu_X`),
96
+ where each point in `Xu` is mapped to its closest
97
+ unique point in `candidates`.
98
+ - If `candidate_labels` is provided:
99
+ Tuple[np.ndarray, np.ndarray]: (`Xu_X`, `Xu_y`).
100
+ `Xu_X` (np.ndarray): (m, d); The matched discrete locations.
101
+ `Xu_y` (np.ndarray): (m, 1); The labels corresponding to `Xu_X`.
102
+
103
+ Usage:
104
+ ```python
105
+ import numpy as np
106
+ from sgptools.utils.misc import cont2disc
107
+
108
+ # Example continuous points
109
+ continuous_points = np.array([[0.1, 0.1], [0.9, 0.9], [0.5, 0.5]])
110
+ # Example discrete candidates
111
+ discrete_candidates = np.array([[0.0, 0.0], [1.0, 1.0], [0.4, 0.6]])
112
+ # Example candidate labels (optional)
113
+ discrete_labels = np.array([[10.0], [20.0], [15.0]])
114
+
115
+ # 1. Map without labels
116
+ mapped_points = cont2disc(continuous_points, discrete_candidates)
117
+
118
+ # 2. Map with labels
119
+ mapped_points_X, mapped_points_y = cont2disc(continuous_points, discrete_candidates, discrete_labels)
120
+ ```
53
121
  """
54
- # Sanity check to ensure that there are sensing locations and candidates to match
55
- if len(candidates)==0 or len(Xu)==0:
122
+ # Sanity check to handle empty inputs gracefully
123
+ if len(candidates) == 0 or len(Xu) == 0:
56
124
  if candidate_labels is not None:
57
- return [], []
125
+ return np.empty((0, Xu.shape[1])), np.empty((0, 1))
58
126
  else:
59
- return []
60
-
127
+ return np.empty((0, Xu.shape[1]))
128
+
129
+ # Compute pairwise Euclidean distances between candidates and Xu
61
130
  dists = pairwise_distances(candidates, Y=Xu, metric='euclidean')
62
- row_ind, _ = linear_sum_assignment(dists)
131
+
132
+ # Use the Hungarian algorithm (linear_sum_assignment) to find the optimal
133
+ # assignment of rows (candidates) to columns (Xu points) that minimizes
134
+ # the total cost (distances). `row_ind` gives the indices of the rows
135
+ # (candidates) chosen, `col_ind` gives the corresponding indices of `Xu`.
136
+ row_ind, col_ind = linear_sum_assignment(dists)
137
+
138
+ # Select the candidate locations that were matched to Xu points
63
139
  Xu_X = candidates[row_ind].copy()
140
+
64
141
  if candidate_labels is not None:
142
+ # If labels are provided, select the corresponding labels as well
65
143
  Xu_y = candidate_labels[row_ind].copy()
66
144
  return Xu_X, Xu_y
67
145
  else:
68
146
  return Xu_X
69
147
 
70
- def plot_paths(paths, candidates=None, title=None):
71
- """Function to plot the IPP solution paths
72
148
 
73
- Args:
74
- paths (ndarray): (r, m, 2); `r` paths with `m` waypoints each
75
- candidates (ndarray): (n, 2); Candidate unlabeled locations used in the SGP-based sensor placement approach
76
- title (str): Title of the plot
149
+ def polygon2candidates(vertices: np.ndarray,
150
+ num_samples: int = 5000,
151
+ random_seed: Optional[int] = None) -> np.ndarray:
77
152
  """
78
- plt.figure()
79
- for i, path in enumerate(paths):
80
- plt.plot(path[:, 0], path[:, 1],
81
- c='r', label='Path', zorder=1, marker='o')
82
- plt.scatter(path[0, 0], path[0, 1],
83
- c='g', label='Start', zorder=2, marker='o')
84
- if candidates is not None:
85
- plt.scatter(candidates[:, 0], candidates[:, 1],
86
- c='k', s=1, label='Unlabeled Train-Set Points', zorder=0)
87
- if i==0:
88
- plt.legend(bbox_to_anchor=(1.0, 1.02))
89
- if title is not None:
90
- plt.title(title)
91
- plt.gca().set_aspect('equal')
92
- plt.xlabel('X')
93
- plt.ylabel('Y')
94
-
95
- def interpolate_path(waypoints, sampling_rate=0.05):
96
- """Interpolate additional points between the given waypoints to simulate continuous sensing robots
153
+ Samples a specified number of candidate points randomly within a polygon defined by its vertices.
154
+ This function leverages `geopandas` for geometric operations.
97
155
 
98
156
  Args:
99
- waypoints (n, d): Waypoints of the robot's path
100
- sampling_rate (float): Distance between each pair of interpolated points
157
+ vertices (np.ndarray): (v, 2); A NumPy array where each row represents the (x, y)
158
+ coordinates of a vertex defining the polygon. `v` is the
159
+ number of vertices. The polygon is closed automatically if
160
+ the first and last vertices are not identical.
161
+ num_samples (int): The desired number of candidate points to sample within the polygon.
162
+ Defaults to 5000.
163
+ random_seed (Optional[int]): Seed for reproducibility of the random point sampling.
164
+ Defaults to None.
101
165
 
102
166
  Returns:
103
- path (ndarray): (p, d) Interpolated path, `p` depends on the sampling_rate rate
104
- """
105
- interpolated_path = []
106
- for i in range(2, len(waypoints)+1):
107
- dist = get_distance(waypoints[i-2:i])
108
- num_samples = int(dist / sampling_rate)
109
- points = np.linspace(waypoints[i-1], waypoints[i-2], num_samples)
110
- interpolated_path.extend(points)
111
- return np.array(interpolated_path)
112
-
113
- def _reoder_path(path, waypoints):
114
- """Reorder the waypoints to match the order of the points in the path.
115
- The waypoints are mathched to the closest points in the path. Used by project_waypoints.
167
+ np.ndarray: (n, 2); A NumPy array where each row represents the (x, y) coordinates
168
+ of a sampled candidate sensor placement location. `n` is `num_samples`.
116
169
 
117
- Args:
118
- path (n, d): Robot path, i.e., waypoints in the path traversal order
119
- waypoints (n, d): Waypoints that need to be reordered to match the target path
120
-
121
- Returns:
122
- waypoints (n, d): Reordered waypoints of the robot's path
123
- """
124
- dists = pairwise_distances(path, Y=waypoints, metric='euclidean')
125
- _, col_ind = linear_sum_assignment(dists)
126
- Xu = waypoints[col_ind].copy()
127
- return Xu
128
-
129
- def project_waypoints(waypoints, candidates):
130
- """Project the waypoints back to the candidate set while retaining the
131
- waypoint visitation order.
170
+ Usage:
171
+ ```python
172
+ import numpy as np
173
+ # from sgptools.utils.misc import polygon2candidates
132
174
 
133
- Args:
134
- waypoints (n, d): Waypoints of the robot's path
135
- candidates (ndarray): (n, 2); Discrete set of candidate locations
175
+ # Define vertices for a square polygon
176
+ square_vertices = np.array([[0, 0], [10, 0], [10, 10], [0, 10]])
136
177
 
137
- Returns:
138
- waypoints (n, d): Projected waypoints of the robot's path
178
+ # Sample 100 candidate points within the square
179
+ sampled_candidates = polygon2candidates(square_vertices, num_samples=100, random_seed=42)
180
+ ```
139
181
  """
140
- waypoints_disc = cont2disc(waypoints, candidates)
141
- waypoints_valid = _reoder_path(waypoints, waypoints_disc)
142
- return waypoints_valid
182
+ # Create a shapely Polygon object from the provided vertices
183
+ poly = geometry.Polygon(vertices)
143
184
 
144
- def ploygon2candidats(vertices,
145
- num_samples=5000,
146
- random_seed=2024):
147
- """Sample unlabeled candidates within a polygon
185
+ # Create a GeoSeries containing the polygon, which enables sampling points
186
+ sampler = gpd.GeoSeries([poly])
148
187
 
149
- Args:
150
- vertices (ndarray): (v, 2) of vertices that define the polygon
151
- num_samples (int): Number of samples to generate
152
- random_seed (int): Random seed for reproducibility
188
+ # Sample random points within the polygon
189
+ candidates_geoseries = sampler.sample_points(
190
+ size=num_samples,
191
+ rng=random_seed) # `rng` is for random number generator seed
153
192
 
154
- Returns:
155
- candidates (ndarray): (n, 2); Candidate sensor placement locations
156
- """
157
- poly = geometry.Polygon(vertices)
158
- sampler = gpd.GeoSeries([poly])
159
- candidates = sampler.sample_points(size=num_samples,
160
- rng=random_seed)
161
- candidates = candidates.get_coordinates().to_numpy()
162
- return candidates
193
+ # Extract coordinates from the GeoSeries of points and convert to a NumPy array
194
+ candidates_array = candidates_geoseries.get_coordinates().to_numpy()
195
+
196
+ return candidates_array