|
4 | 4 | from math import radians, cos, sin, asin, sqrt |
5 | 5 |
|
6 | 6 | import numpy as np |
| 7 | +from sklearn.cluster import DBSCAN |
7 | 8 |
|
8 | 9 | from grid_reducer.altdss.altdss_models import Circuit |
9 | 10 |
|
@@ -163,3 +164,104 @@ def get_circuit_noisy_distances( |
163 | 164 | d = coord_distance(orig_bus.X, orig_bus.Y, noisy_bus.X, noisy_bus.Y, is_geo) |
164 | 165 | dists.append(d) |
165 | 166 | return dists |
| 167 | + |
| 168 | + |
| 169 | +def get_cluster_dp_circuit(circuit: Circuit, noise_config: BasePrivacyConfig, cluster_eps=0.1, min_samples=3): |
| 170 | + """ |
| 171 | + Clusters geo-coordinates, adds Laplace noise to cluster centroids, |
| 172 | + assigns noisy centroid to all buses in cluster. |
| 173 | + """ |
| 174 | + coords = [(bus.X, bus.Y) for bus in circuit.Bus if bus.X is not None and bus.Y is not None] |
| 175 | + clustering = DBSCAN(eps=cluster_eps, min_samples=min_samples).fit(coords) |
| 176 | + labels = clustering.labels_ |
| 177 | + clusters = {} |
| 178 | + for idx, label in enumerate(labels): |
| 179 | + clusters.setdefault(label, []).append(coords[idx]) |
| 180 | + noisy_coords = [] |
| 181 | + for _idx, label in enumerate(labels): |
| 182 | + cluster_points = clusters[label] |
| 183 | + # Compute centroid for cluster |
| 184 | + centroid = np.mean(cluster_points, axis=0) |
| 185 | + noisy_centroid = apply_planar_laplace_noise( |
| 186 | + centroid[0], centroid[1], int(noise_config.geo_coordinate_noise) |
| 187 | + ) |
| 188 | + noisy_coords.append(noisy_centroid) |
| 189 | + # Build new circuit with bus coordinates replaced |
| 190 | + new_buses = [] |
| 191 | + i = 0 |
| 192 | + for bus in circuit.Bus: |
| 193 | + new_bus = copy.deepcopy(bus) |
| 194 | + if bus.X is not None and bus.Y is not None: |
| 195 | + new_bus.X, new_bus.Y = noisy_coords[i] |
| 196 | + i += 1 |
| 197 | + new_buses.append(new_bus) |
| 198 | + new_circuit = copy.deepcopy(circuit) |
| 199 | + new_circuit.Bus = new_buses |
| 200 | + return new_circuit |
| 201 | + |
| 202 | +def apply_adaptive_noise(x, y, coords, base_epsilon, min_epsilon=500, max_epsilon=5000, neighbor_radius=0.01): |
| 203 | + """ |
| 204 | + Inject Laplace noise with adaptive scaling based on local density. |
| 205 | + Points in denser areas get lower noise; sparse areas get higher noise. |
| 206 | + """ |
| 207 | + # Count neighbors within 'neighbor_radius' |
| 208 | + count = sum( |
| 209 | + math.sqrt((x - x2)**2 + (y - y2)**2) < neighbor_radius |
| 210 | + for x2, y2 in coords if (x2, y2) != (x, y) |
| 211 | + ) |
| 212 | + # More neighbors --> less noise; fewer neighbors --> more noise |
| 213 | + density_score = count / len(coords) |
| 214 | + # Invert: sparse areas (density_score→0) => max_epsilon; dense areas (density_score→1) => min_epsilon |
| 215 | + adaptive_epsilon = min_epsilon + (max_epsilon - min_epsilon) * (1 - density_score) |
| 216 | + return apply_planar_laplace_noise(x, y, adaptive_epsilon) |
| 217 | + |
| 218 | +def get_adaptive_dp_circuit(circuit: Circuit, base_epsilon=3500, min_epsilon=500, max_epsilon=5000, neighbor_radius=0.01): |
| 219 | + coords = [(bus.X, bus.Y) for bus in circuit.Bus if bus.X is not None and bus.Y is not None] |
| 220 | + new_buses = [] |
| 221 | + for bus in circuit.Bus: |
| 222 | + new_bus = copy.deepcopy(bus) |
| 223 | + if new_bus.X is not None and new_bus.Y is not None: |
| 224 | + new_bus.X, new_bus.Y = apply_adaptive_noise( |
| 225 | + new_bus.X, new_bus.Y, coords, base_epsilon, min_epsilon, max_epsilon, neighbor_radius |
| 226 | + ) |
| 227 | + new_buses.append(new_bus) |
| 228 | + new_circuit = copy.deepcopy(circuit) |
| 229 | + new_circuit.Bus = new_buses |
| 230 | + return new_circuit |
| 231 | + |
| 232 | +def evaluate_dp_methods_on_circuit( |
| 233 | + circuit: Circuit, |
| 234 | + noise_config: BasePrivacyConfig, |
| 235 | + cluster_eps=0.1, |
| 236 | + min_samples=3, |
| 237 | + adaptive_params=None |
| 238 | +): |
| 239 | + # Planar Laplace mechanism |
| 240 | + laplace_circuit = get_dp_circuit(circuit, noise_config) |
| 241 | + laplace_dists = get_circuit_noisy_distances(circuit, laplace_circuit) |
| 242 | + |
| 243 | + # Cluster-based mechanism |
| 244 | + cluster_circuit = get_cluster_dp_circuit(circuit, noise_config, cluster_eps, min_samples) |
| 245 | + cluster_dists = get_circuit_noisy_distances(circuit, cluster_circuit) |
| 246 | + |
| 247 | + # Adaptive noise mechanism |
| 248 | + if adaptive_params is None: |
| 249 | + adaptive_params = dict(base_epsilon=noise_config.geo_coordinate_noise, min_epsilon=500, max_epsilon=5000, neighbor_radius=0.01) |
| 250 | + adaptive_circuit = get_adaptive_dp_circuit(circuit, **adaptive_params) |
| 251 | + adaptive_dists = get_circuit_noisy_distances(circuit, adaptive_circuit) |
| 252 | + |
| 253 | + result = { |
| 254 | + "planar_mean_loss": np.mean(laplace_dists), |
| 255 | + "planar_median_loss": np.median(laplace_dists), |
| 256 | + "cluster_mean_loss": np.mean(cluster_dists), |
| 257 | + "cluster_median_loss": np.median(cluster_dists), |
| 258 | + "adaptive_mean_loss": np.mean(adaptive_dists), |
| 259 | + "adaptive_median_loss": np.median(adaptive_dists), |
| 260 | + "planar_dists": laplace_dists, |
| 261 | + "cluster_dists": cluster_dists, |
| 262 | + "adaptive_dists": adaptive_dists, |
| 263 | + "planar_circuit": laplace_circuit, |
| 264 | + "cluster_circuit": cluster_circuit, |
| 265 | + "adaptive_circuit": adaptive_circuit, |
| 266 | + } |
| 267 | + return result |
0 commit comments