|
| 1 | +""" |
| 2 | +Traffic grooming module for optical network simulations. |
| 3 | +
|
| 4 | +This module provides traffic grooming functionality to efficiently pack |
| 5 | +multiple requests onto existing lightpaths, improving resource utilization. |
| 6 | +""" |
| 7 | + |
| 8 | +from typing import Any |
| 9 | + |
| 10 | +from fusion.core.properties import GroomingProps |
| 11 | +from fusion.utils.logging_config import get_logger |
| 12 | + |
| 13 | +logger = get_logger(__name__) |
| 14 | + |
| 15 | + |
| 16 | +class Grooming: |
| 17 | + """ |
| 18 | + Traffic grooming handler for optical networks. |
| 19 | +
|
| 20 | + This class manages the grooming of network requests to existing lightpaths, |
| 21 | + including partial grooming and service release operations. |
| 22 | +
|
| 23 | + :param engine_props: Engine configuration properties |
| 24 | + :type engine_props: dict[str, Any] |
| 25 | + :param sdn_props: SDN controller properties |
| 26 | + :type sdn_props: Any |
| 27 | + """ |
| 28 | + |
| 29 | + def __init__(self, engine_props: dict[str, Any], sdn_props: Any) -> None: |
| 30 | + """ |
| 31 | + Initialize grooming handler. |
| 32 | +
|
| 33 | + :param engine_props: Engine configuration dictionary |
| 34 | + :type engine_props: dict[str, Any] |
| 35 | + :param sdn_props: SDN properties object |
| 36 | + :type sdn_props: Any |
| 37 | + """ |
| 38 | + self.grooming_props = GroomingProps() |
| 39 | + self.engine_props = engine_props |
| 40 | + self.sdn_props = sdn_props |
| 41 | + |
| 42 | + logger.debug("Initialized grooming handler") |
| 43 | + |
| 44 | + def _find_path_max_bw(self, light_id: tuple) -> dict[str, Any] | None: |
| 45 | + """ |
| 46 | + Find the path group with the maximum total remaining bandwidth. |
| 47 | +
|
| 48 | + Groups lightpaths by their physical path and returns the group |
| 49 | + with the highest total remaining bandwidth. Skips degraded lightpaths. |
| 50 | +
|
| 51 | + :param light_id: Tuple of (source, destination) representing the path ID |
| 52 | + :type light_id: tuple |
| 53 | + :return: Dictionary of the path group with maximum remaining bandwidth or None |
| 54 | + :rtype: dict[str, Any] | None |
| 55 | + """ |
| 56 | + path_groups: dict[tuple, dict[str, Any]] = {} |
| 57 | + |
| 58 | + for lp_id, lp_info in self.sdn_props.lightpath_status_dict[light_id].items(): |
| 59 | + # Skip lightpaths with degraded SNRs |
| 60 | + if lp_info.get("is_degraded", False): |
| 61 | + continue |
| 62 | + |
| 63 | + if lp_info["remaining_bandwidth"] > 0: |
| 64 | + path_key = tuple(lp_info["path"]) |
| 65 | + reverse_path_key = tuple(reversed(lp_info["path"])) |
| 66 | + |
| 67 | + # Normalize path key (treat path and reverse as same group) |
| 68 | + normalized_path_key = min(path_key, reverse_path_key) |
| 69 | + |
| 70 | + if normalized_path_key not in path_groups: |
| 71 | + path_groups[normalized_path_key] = { |
| 72 | + "total_remaining_bandwidth": 0, |
| 73 | + "lightpaths": [], |
| 74 | + "lp_id_list": [], |
| 75 | + } |
| 76 | + |
| 77 | + path_groups[normalized_path_key]["total_remaining_bandwidth"] += ( |
| 78 | + lp_info["remaining_bandwidth"] |
| 79 | + ) |
| 80 | + path_groups[normalized_path_key]["lightpaths"].append((lp_id, lp_info)) |
| 81 | + path_groups[normalized_path_key]["lp_id_list"].append(lp_id) |
| 82 | + |
| 83 | + # Find the path group with the maximum total remaining bandwidth |
| 84 | + if not path_groups: |
| 85 | + return None |
| 86 | + |
| 87 | + return max( |
| 88 | + path_groups.values(), |
| 89 | + key=lambda group: int(group["total_remaining_bandwidth"]), |
| 90 | + default=None, |
| 91 | + ) |
| 92 | + |
| 93 | + def _end_to_end_grooming(self) -> bool: |
| 94 | + """ |
| 95 | + Groom arrival requests to already established lightpaths. |
| 96 | +
|
| 97 | + Attempts to allocate the requested bandwidth using existing lightpaths |
| 98 | + between the source and destination. Supports partial grooming where |
| 99 | + part of the request is groomed and the remainder requires new lightpaths. |
| 100 | +
|
| 101 | + :return: True if the request was fully groomed, False otherwise |
| 102 | + :rtype: bool |
| 103 | + """ |
| 104 | + light_id = tuple(sorted([self.sdn_props.source, self.sdn_props.destination])) |
| 105 | + |
| 106 | + if light_id not in self.sdn_props.lightpath_status_dict: |
| 107 | + return False |
| 108 | + |
| 109 | + max_path_group = self._find_path_max_bw(light_id) |
| 110 | + if not max_path_group or max_path_group["total_remaining_bandwidth"] == 0: |
| 111 | + return False |
| 112 | + |
| 113 | + remaining_bw = int(self.sdn_props.bandwidth) |
| 114 | + |
| 115 | + for lp_id in max_path_group["lp_id_list"]: |
| 116 | + lp_info = self.sdn_props.lightpath_status_dict[light_id][lp_id] |
| 117 | + |
| 118 | + if lp_info["remaining_bandwidth"] == 0: |
| 119 | + continue |
| 120 | + |
| 121 | + # Determine how much bandwidth to allocate from this lightpath |
| 122 | + if lp_info["remaining_bandwidth"] > remaining_bw: |
| 123 | + tmp_remaining_bw = remaining_bw |
| 124 | + remaining_bw = 0 |
| 125 | + else: |
| 126 | + tmp_remaining_bw = lp_info["remaining_bandwidth"] |
| 127 | + remaining_bw -= lp_info["remaining_bandwidth"] |
| 128 | + self.sdn_props.is_sliced = True |
| 129 | + |
| 130 | + # Update lightpath status |
| 131 | + lp_info["requests_dict"].update( |
| 132 | + {self.sdn_props.request_id: tmp_remaining_bw} |
| 133 | + ) |
| 134 | + lp_info["remaining_bandwidth"] -= tmp_remaining_bw |
| 135 | + |
| 136 | + # Calculate utilization percentage |
| 137 | + lp_usage = 1 - ( |
| 138 | + lp_info["remaining_bandwidth"] / lp_info["lightpath_bandwidth"] |
| 139 | + ) |
| 140 | + lp_info["time_bw_usage"].update({self.sdn_props.arrive: lp_usage * 100}) |
| 141 | + |
| 142 | + # Update SDN properties with this lightpath's allocation |
| 143 | + self.sdn_props.bandwidth_list.append(str(tmp_remaining_bw)) |
| 144 | + self.sdn_props.core_list.append(lp_info["core"]) |
| 145 | + self.sdn_props.band_list.append(lp_info["band"]) |
| 146 | + self.sdn_props.start_slot_list.append(lp_info["start_slot"]) |
| 147 | + self.sdn_props.end_slot_list.append(lp_info["end_slot"]) |
| 148 | + self.sdn_props.modulation_list.append(lp_info["mod_format"]) |
| 149 | + self.sdn_props.path_list = lp_info["path"] |
| 150 | + self.sdn_props.crosstalk_list.append(lp_info["snr_cost"]) |
| 151 | + self.sdn_props.lightpath_bandwidth_list.append( |
| 152 | + lp_info["lightpath_bandwidth"] |
| 153 | + ) |
| 154 | + self.sdn_props.lightpath_id_list.append(lp_id) |
| 155 | + self.sdn_props.path_weight = lp_info["path_weight"] |
| 156 | + |
| 157 | + if remaining_bw == 0: |
| 158 | + # Fully groomed - no new lightpath needed |
| 159 | + self.sdn_props.was_routed = True |
| 160 | + self.sdn_props.was_groomed = True |
| 161 | + self.sdn_props.was_partially_groomed = False |
| 162 | + self.sdn_props.number_of_transponders = 0 |
| 163 | + self.sdn_props.was_new_lp_established = [] |
| 164 | + self.sdn_props.remaining_bw = "0" |
| 165 | + logger.debug( |
| 166 | + "Request %s fully groomed using %d lightpaths", |
| 167 | + self.sdn_props.request_id, |
| 168 | + len(max_path_group["lp_id_list"]), |
| 169 | + ) |
| 170 | + return True |
| 171 | + |
| 172 | + # Partially groomed - some bandwidth still needs allocation |
| 173 | + self.sdn_props.was_partially_groomed = True |
| 174 | + self.sdn_props.was_groomed = False |
| 175 | + self.sdn_props.remaining_bw = remaining_bw |
| 176 | + logger.debug( |
| 177 | + "Request %s partially groomed, %d bandwidth remaining", |
| 178 | + self.sdn_props.request_id, |
| 179 | + remaining_bw, |
| 180 | + ) |
| 181 | + return False |
| 182 | + |
| 183 | + def _release_service(self) -> list[int]: |
| 184 | + """ |
| 185 | + Remove a previously allocated request from the lightpaths. |
| 186 | +
|
| 187 | + Frees up bandwidth on lightpaths that were used by this request |
| 188 | + and identifies lightpaths that are now completely unused. |
| 189 | +
|
| 190 | + :return: List of lightpath IDs that are no longer carrying any requests |
| 191 | + :rtype: list[int] |
| 192 | + """ |
| 193 | + release_lp = [] |
| 194 | + light_id = tuple(sorted([self.sdn_props.source, self.sdn_props.destination])) |
| 195 | + |
| 196 | + for lp_id in self.sdn_props.lightpath_id_list[:]: |
| 197 | + index = self.sdn_props.lightpath_id_list.index(lp_id) |
| 198 | + lp_info = self.sdn_props.lightpath_status_dict[light_id][lp_id] |
| 199 | + |
| 200 | + # Get allocated bandwidth for this request |
| 201 | + req_bw = lp_info["requests_dict"][self.sdn_props.request_id] |
| 202 | + |
| 203 | + # Remove request from lightpath |
| 204 | + lp_info["requests_dict"].pop(self.sdn_props.request_id) |
| 205 | + lp_info["remaining_bandwidth"] += req_bw |
| 206 | + self.sdn_props.remaining_bw = int(self.sdn_props.remaining_bw) - req_bw |
| 207 | + |
| 208 | + # Clean up tracking lists |
| 209 | + self.sdn_props.lightpath_id_list.pop(index) |
| 210 | + self.sdn_props.lightpath_bandwidth_list.pop(index) |
| 211 | + |
| 212 | + # Check if lightpath is now completely unused |
| 213 | + if lp_info["remaining_bandwidth"] == lp_info["lightpath_bandwidth"]: |
| 214 | + release_lp.append(lp_id) |
| 215 | + else: |
| 216 | + # Update utilization for partially used lightpath |
| 217 | + lp_usage = 1 - ( |
| 218 | + lp_info["remaining_bandwidth"] / lp_info["lightpath_bandwidth"] |
| 219 | + ) |
| 220 | + lp_info["time_bw_usage"].update({self.sdn_props.depart: lp_usage * 100}) |
| 221 | + |
| 222 | + logger.debug( |
| 223 | + "Released request %s from %d lightpaths, %d lightpaths now empty", |
| 224 | + self.sdn_props.request_id, |
| 225 | + len(self.sdn_props.lightpath_id_list), |
| 226 | + len(release_lp), |
| 227 | + ) |
| 228 | + return release_lp |
| 229 | + |
| 230 | + def handle_grooming(self, request_type: str) -> bool | list[int]: |
| 231 | + """ |
| 232 | + Control grooming operations based on request type. |
| 233 | +
|
| 234 | + Entry point for grooming operations. Routes to appropriate |
| 235 | + method based on whether this is an arrival or release request. |
| 236 | +
|
| 237 | + :param request_type: Type of request ("arrival" or "release") |
| 238 | + :type request_type: str |
| 239 | + :return: For arrivals: bool indicating if fully groomed. |
| 240 | + For releases: list of lightpath IDs to release. |
| 241 | + :rtype: bool | list[int] |
| 242 | + """ |
| 243 | + if request_type == "release": |
| 244 | + return self._release_service() |
| 245 | + return self._end_to_end_grooming() |
0 commit comments