Intelligent Scheduling, Efficient Empowerment: Unveiling the Path to Optimal Resource Allocation in Computing Power Networks#

A computing power network, in simple terms, is a network that delivers computational power just like electricity or water. Imagine that the phones, computers, and other devices we use daily all require processors to compute and run various programs. The computing power network is like a massive ‘processor warehouse’ that consolidates scattered computing resources and optimizes their distribution via the internet to meet the computational needs of different users and scenarios.

For example, when we watch high-definition videos, play large-scale games, or perform complex data analysis, these operations require substantial computing power. The computing power network acts like an intelligent ‘power system’ that can quickly provide the necessary computational resources, ensuring a smooth and efficient user experience. This way, we can enjoy powerful computing capabilities anytime and anywhere without worrying about insufficient device performance.

Challenges Faced by Computing Power Networks#

  • Resource Allocation Efficiency: A computing power network needs to efficiently allocate computational resources to meet the diverse needs of different users and applications. This requires the network to monitor resource usage in real-time and dynamically adjust resource allocation strategies to avoid resource idleness or overload.

  • Network Latency: Quick response to computational tasks requires a low-latency network environment. A computing power network must address the latency issues in data transmission, especially when handling applications with high real-time requirements, such as autonomous driving.

  • Low Cost: Building and maintaining a computing power network requires significant financial investment, so it is necessary to minimize costs while ensuring performance.

Components of a Computing Power Network#

A computing power network mainly consists of three parts:

  • Edge Devices: These are various smart devices that are responsible for collecting data.

  • Edge Servers: Located near the devices, they are responsible for processing part of the data and alleviating the burden on cloud servers.

  • Cloud Servers: Located in data centers, they have powerful computing capabilities and are responsible for processing more complex data.

Problem Description#

Consider an optimization problem for the computing power network layout in a specific area. The area is divided into several adjacent square grids, and the computing power demand distribution data provides the computing power demand within each grid. The coordinate values in the data represent the center coordinates of the grid. To simplify the problem, the computing power demand points within each grid are unified to the grid center point (i.e., one grid corresponds to one demand point).

The computing power demand within the grid is generated by edge devices, which are terminal devices connected to the network, such as sensors, smartphones, and industrial robots. The computing power demand in the computing power network is met by edge servers and cloud servers. Edge servers are located at the ‘edge’ of the network, usually close to the end users or devices. Their task is to process data closer to the user to improve response speed and efficiency. Edge servers can process requests faster because they are closer to the users. Edge servers can also alleviate the burden on the core cloud infrastructure, improving overall operational efficiency. Cloud servers are located in data centers far from users and have powerful computing and storage capabilities. When the capacity of edge servers is insufficient, cloud servers can serve as a supplement. The collaboration between edge servers and cloud servers can optimize the performance and reliability of the entire system.

This is an image description

Problem 1: In Problem 1, we only consider meeting the demand within the computing region using edge servers. Suppose we want to set up 2 edge servers within the grid area with computing power demand distribution, with each edge server having a coverage radius of 1. A QUBO model is required to determine at which points to place the edge servers to cover the maximum computing power demand.

Question 2: When the edge servers cannot meet the computational demand, upstream cloud servers will provide the necessary computing services. Now, a cloud server is added outside the grid area, and both end nodes and edge servers can choose to connect to the cloud node. When the computational demand received by the edge server exceeds its capacity, the excess demand will be directly allocated to the cloud server. The computational demand of each end node must be met and can only be served by one server, which can be either a cloud node or an edge server. Since edge servers have a resource capacity limit, assume that the available computational resource capacity of each edge server is 12, and the available computational resource capacity of the cloud server is infinite, meaning that the cloud server’s available computational resource capacity is not considered. Servers have a certain coverage radius, with the edge server’s coverage radius assumed to be 3, and the cloud server’s coverage radius is infinite, meaning that the cloud server’s coverage range is not considered.

Establishing edge servers typically incurs costs, which consist of fixed costs, computation costs, and transmission costs. The fixed cost is related to whether and where the edge server is established. The computation cost is proportional to the amount of requested computational resources, calculated as the unit computation cost multiplied by the computation amount. The unit computation cost for cloud servers is 1, while for edge servers, it is 2. Additionally, there is a transmission cost between the end node and the edge, from the edge to the cloud, and from the end to the cloud. The transmission cost is calculated by multiplying the computational demand by the transmission distance and the unit transmission cost. The transmission distance is calculated using the Euclidean distance, rounded to two decimal places, and calculated as a one-way distance (round-trip transmission is not considered). The unit transmission cost from the end to the edge and from the edge to the cloud is 1, while the unit transmission cost from the end to the cloud is 2.

To meet all the end-side computational demands within the area, establish a QUBO model to solve for the network layout that minimizes the overall cost, including the location and number of edge servers, and the connections between end-to-edge, edge-to-cloud, and end-to-cloud nodes.

The essence of the problem#

In the problem of computational network layout optimization, on the surface, we may think of classical resource scheduling or network flow optimization problems and try to solve ‘how to allocate resources among different areas to minimize costs’ using traditional greedy algorithms or dynamic programming methods. These methods might help us find a local optimal solution at a specific moment, but the complexity of this problem goes far beyond the surface. We not only need to consider the resource allocation of each computational node but also globally optimize the entire network layout, ensuring that each resource is reasonably utilized and that efficient computation and communication are maintained across multiple regions.

In-depth analysis#

The core of the computational network layout optimization problem is how to find an optimal resource allocation and node layout scheme across the entire network, similar to finding the optimal computational node layout and communication path combination in graph G. This is essentially a combinatorial optimization problem, requiring the selection of the optimal solution from all possible network layouts, and the number of these possible layouts is enormous. For example, if we have n potential edge server deployment locations and m computational task points, the combination of these locations and tasks will grow exponentially, with n^m possible layout combinations.

Obviously, a simple brute-force method is unrealistic because even for a moderately sized network, the number of combinations is astronomical, far beyond the capabilities of classical computers. Just like the path selection problem in the traveling salesman problem, we face a combinatorial challenge here, where every node and resource configuration in the network could affect the overall optimization result.

In other words, as the problem size increases, the time complexity of the algorithm grows non-polynomially, making it difficult to solve within a reasonable time. Therefore, the key to the problem is not how to locally optimize a single resource allocation, but how to globally optimize the entire computational network layout, finding the optimal layout path that minimizes overall computation costs. The complexity of this problem lies not only in the increase in computation but also in the need for multi-dimensional optimization to find a global optimal solution.

Reference model for the first question#

Restatement of the first question#

In a 4x4 grid, each grid represents an area with computational demand. Our goal is to determine in which grids to deploy edge computing nodes to maximize the covered user demand.

Symbol Definitions#

  • R=1R=1: Coverage radius of edge computing nodes

  • P=2P=2: Number of planned edge computing nodes to be deployed.

  • Lu\mathcal{L}_{u}: Set of user locations.

  • Le\mathcal{L}_{e}: Set of candidate edge node locations.

  • demandl(lLu)\text{demand}_{l} (l\in \mathcal{L}_{u}): Computational demand at grid ll.

  • dijd_{ij}: Distance between grid ii and grid jj.

  • αij=1dijR\alpha_{ij}=\mathbb{1}_{d_{ij}\leq R}: Indicates whether the distance between grid ii and grid jj does not exceed the coverage radius RR of the edge node.

Decision Variables#

  • xix_{i}: A binary variable indicating whether to deploy an edge computing node in grid ii.

  • ziz_{i}: A binary variable indicating whether grid ii is covered.

Objective Function#

Maximize the total covered computational demand:

maxiLuzidemandi\max \sum_{i\in \mathcal{L}_{u}} {z_{i}\cdot \text{demand}_{i}}

Constraints#

  • The coverage status of each user grid does not exceed the coverage status of the surrounding edge nodes:

    zijLeαijxj,iLuz_{i}\leq \sum_{j \in \mathcal{L}_{e}}{\alpha_{ij}\cdot x_{j}}, \forall i\in \mathcal{L}_{u}
  • The total number of deployed edge computing nodes is equal to PP:

    jLexj=P\sum_{j\in \mathcal{L}_{e}} x_{j} = P

Simplified Model (Inclusion-Exclusion Principle)#

When P=2P=2, we can simplify the model using the inclusion-exclusion principle:

maxiLu(jLeαijxjj,kLeαijαikxjxk)demandis.t.jLexj=2\begin{aligned} \max \quad & \sum_{i\in \mathcal{L}_{u}} \left( \sum_{j\in \mathcal{L}_{e}} \alpha_{ij} x_j - \sum_{j,k\in \mathcal{L}_{e}} \alpha_{ij}\alpha_{ik} x_j x_k \right) \mathrm{demand}_i \\[0.5ex] \text{s.t.} \quad & \sum_{j\in \mathcal{L}_{e}} x_j = 2 \end{aligned}

Reference Model for Question 2#

Problem Overview#

  • A 6×6 grid where each grid has a certain computational demand (only 11 grids have non-zero demand), and all computational demands need to be met.

  • There are 5 candidate locations for edge servers, each with limited computational capacity. When the computational requests received by an edge server exceed its capacity, the excess requests are sent to the cloud server.

  • One location hosts a cloud server, which has no capacity limit.

  • Users can connect to either the edge server or the cloud server (but only one); if the edge server’s capacity is insufficient, it can connect to the cloud server.

  • Minimize cost: fixed cost + variable cost (computational cost) + transmission cost (= unit cost * distance * transmission volume)

Schematic diagram

Symbol Definitions#

  • CedgeC_{\text{edge}}: Edge server capacity.

  • c-fix-edgej\text{c-fix-edge}_{j}: Fixed cost of establishing an edge computing node in grid jj.

  • c-var-cloud\text{c-var-cloud}: Unit computational cost at the cloud node.

  • c-var-edge\text{c-var-edge}: Unit computational cost at the edge node.

  • c-tranu,e\text{c-tran}_{u,e}: Unit transmission cost from user side to edge side.

  • c-tranu,c\text{c-tran}_{u,c}: Unit transmission cost from user side to cloud side.

  • c-trane,c\text{c-tran}_{e,c}: Unit transmission cost from edge side to cloud side.

  • dijd_{ij}: Distance between grid ii and grid jj.

  • did_{i}: The distance between grid ii and the cloud server.

  • αij=1dijR\alpha_{ij}=\mathbb{1}_{d_{ij} \leq R}: Whether the distance between grid ii and grid jj does not exceed the edge node coverage radius RR

Intermediate Variables#

  • yiju,ey^{u,e}_{ij}: Whether the demand of grid ii is served by the edge server located at grid jj

  • yje,cy^{e,c}_{j}: Whether the edge server at grid jj is connected to the cloud server.

  • yiu,cy^{u,c}_{i}: Whether the demand of grid ii is served by the cloud server.

  • ujNu_{j} \in \mathbb{N}: The number of computational demands exceeding the capacity of edge server jj that are served by the cloud server.

Decision Variables#

  • xedge,jx_{\text{edge}, j}: Whether to open an edge server at location jj.

Mathematical Model#

Objective Function:

minCostfix+Costvar+Costtran\min \text{Cost}_{\text{fix}} + \text{Cost}_{\text{var}} + \text{Cost}_{\text{tran}}

where

Costfix=jLec-fix-edgejxedge,j\begin{aligned} \text{Cost}_{\text{fix}} = \sum_{j \in \mathcal{L}_{e}} \text{c-fix-edge}_{j}\, x_{\text{edge},j} \end{aligned}
Costvar=  c-var-cloudiLudemandiyiu,c+c-var-edgejLeiLudemandiyiju,e+(c-var-cloudc-var-edge)jLeuj\begin{aligned} \text{Cost}_{\text{var}} =\;& \text{c-var-cloud} \sum_{i \in \mathcal{L}_{u}} \text{demand}_{i}\, y^{u,c}_{i} \\ &+ \text{c-var-edge} \sum_{j \in \mathcal{L}_{e}} \sum_{i \in \mathcal{L}_{u}} \text{demand}_{i}\, y^{u,e}_{ij} \\ &+ (\text{c-var-cloud}-\text{c-var-edge}) \sum_{j \in \mathcal{L}_{e}} u_j \end{aligned}
Costtran=  c-tranu,eiLujLeyiju,edemandidij+c-tranu,ciLuyiu,cdemandidi+c-trane,cjLedjuj\begin{aligned} \text{Cost}_{\text{tran}} =\;& \text{c-tran}_{u,e} \sum_{i \in \mathcal{L}_{u}} \sum_{j \in \mathcal{L}_{e}} y^{u,e}_{ij}\, \text{demand}_{i}\, d_{ij} \\ &+ \text{c-tran}_{u,c} \sum_{i \in \mathcal{L}_{u}} y^{u,c}_{i}\, \text{demand}_{i}\, d_i \\ &+ \text{c-tran}_{e,c} \sum_{j \in \mathcal{L}_{e}} d_j\, u_j \end{aligned}

In this model, we express uju_{j} as

uj=yje,c(iLudemandiyiju,eCedge)u_{j} = y^{e,c}_{j} \cdot \Big(\sum_{i \in \mathcal{L}_{u}} \text{demand}_{i} \cdot y^{u,e}_{ij} - C_{\text{edge}}\Big)

At the same time, we add constraints so that yjku,ey^{u,e}_{jk} will take the value of 11 only when the computational demand received by edge server jj exceeds its capacity limit, otherwise it will be 00. Although the expression of uju_{j} contains quadratic terms, in this model, the cloud server has no capacity limit, and uju_{j} only appears in the objective function, not in the constraints, ensuring that no higher-order terms appear when converted to the QUBO model.

Constraints:

  • The computational demand points are allocated to either edge or cloud and are served by only one.

    jLeyiju,e+yiu,c=1,iLu\sum_{j \in \mathcal{L}_{e}}{y^{u,e}_{ij}} + y^{u,c}_{i} = 1, \forall i \in \mathcal{L}_{u}
  • Coverage relationship, and only if the edge server is opened can it connect from the demand point.

    yiju,eαijxedge,j,iLu,jLey^{u,e}_{ij} \leq \alpha_{ij} \cdot x_{\text{edge}, j}, \forall i \in \mathcal{L}_{u}, j \in \mathcal{L}_{e}
  • The relationship between edge connection to the cloud and opening the edge server.

    yje,cxedge,j,jLey^{e,c}_{j} \leq x_{\text{edge}, j}, \forall j \in \mathcal{L}_{e}
  • Only when the computational demand received by edge server jj exceeds its capacity limit, will yjku,ey^{u,e}_{jk} take the value of 11.

    yje,cCedgeiLudemandiαijyiju,e(1)y^{e,c}_{j} \cdot C_{\text{edge}} \leq \sum_{i \in \mathcal{L}_{u}}{\text{demand}_{i} \cdot \alpha_{ij} \cdot y^{u,e}_{ij}} \quad \text{(1)}
    iLudemandiαijyiju,eCedge(max-uj[j]Cedge)yje,c(2)\sum_{i \in \mathcal{L}_{u}}{\text{demand}_{i} \cdot \alpha_{ij} \cdot y^{u,e}_{ij}} - C_{\text{edge}} \leq (\text{max-uj}[j] - C_{\text{edge}}) \cdot y^{e,c}_{j} \quad \text{(2)}

    where :math:text{max-uj}[j] is the upper bound of the computational demand received by edge server jj, which we set as

    max-uj[j]=iLudemandiαij\text{max-uj}[j] = \sum_{i \in \mathcal{L}_{u}}{\text{demand}_{i} \cdot \alpha_{ij}}

Preprocessing#

For candidate locations of edge servers jj, if the computational demand they receive is always less than the edge server capacity limit, for example,

max-uj[j]Cedge\text{max-uj}[j] \leq C_{\text{edge}}

When this is true, we can set yje,c=0y^{e,c}_{j} = 0, and for this candidate location, inequalities (1) and (2) can be ignored to reduce the number of bits (relaxation variables).

Code for Question 1#

  1import math
  2import numpy as np
  3import kaiwu as kw
  4
  5
  6class EdgeCoveragePlanner:
  7    """Solver for Edge Coverage Planning problem."""
  8
  9    def __init__(
 10        self,
 11        num_nodes: int = 2,
 12        coverage_range: float = 2.0,
 13        penalty: float = 100.0,
 14        num_slack_bins: int = 1,
 15    ) -> None:
 16        """Initialize EdgeCoveragePlanner parameters.
 17
 18        Args:
 19            num_nodes: Number of edge nodes to be selected.
 20
 21            coverage_range: Node coverage threshold.
 22
 23            penalty: penalty coefficient.
 24
 25            num_slack_bins: Slack binary digits, used to override constraints.
 26        """
 27        self.num_nodes = num_nodes
 28        self.coverage_range = coverage_range
 29        self.penalty = penalty
 30        self.num_slack_bins = num_slack_bins
 31
 32        # Data containers
 33        self.demand = {}
 34        self.locations = []
 35        self.distances = {}
 36        self.coverage = {}
 37        self.dimension = 0
 38
 39        # QUBO model
 40        self.model = None
 41
 42    def prepare_data(self, demand_data):
 43        """prepare demand, distance, and coverage matrix data.
 44
 45        Args:
 46            demand_data: dict, the key is the coordinate string 'i, j',
 47                        and the value corresponds to the calculation requirement.
 48        """
 49        self.demand = demand_data
 50        # Create a list containing all location coordinates
 51        self.locations = list(demand_data.keys())
 52        # Calculate the number of location coordinates
 53        self.dimension = int(math.sqrt(len(self.demand)))
 54        # Iterate through all location coordinates to calculate distances between pairs
 55        self.distances = {
 56            (i, j): np.linalg.norm(
 57                np.array([int(coord) for coord in i.split(",")])
 58                - np.array([int(coord) for coord in j.split(",")])
 59            )
 60            for i in self.locations
 61            for j in self.locations
 62        }
 63        # Initialize a dictionary to store the coverage relationship between locations
 64        self.coverage = {
 65            (i, j): int(dist <= self.coverage_range)
 66            for (i, j), dist in self.distances.items()
 67        }
 68
 69    def prepare_model(self):
 70        """Building a Qubo Model"""
 71        # Create binary variable arrays x and z to represent edge computing node locations
 72        # and demand coverage conditions
 73        var_x = kw.core.ndarray((self.dimension, self.dimension), "x", kw.core.Binary)
 74        var_z = kw.core.ndarray((self.dimension, self.dimension), "z", kw.core.Binary)
 75        limit_z = kw.core.zeros((self.dimension, self.dimension))
 76        # Initialize slack variables
 77        slack = kw.core.ndarray(
 78            (self.dimension, self.dimension),
 79            "slack",
 80            kw.core.Integer,
 81            (0, (2**self.num_slack_bins) - 1),
 82        )
 83        # Define the objective function to minimize the total computing power demand
 84        obj = kw.core.quicksum(
 85            self.demand[f"{i + 1},{j + 1}"] * var_z[i, j]
 86            for i in range(self.dimension)
 87            for j in range(self.dimension)
 88        )
 89        # Iterate through all location coordinates to build the second constraint
 90        for i in range(self.dimension):
 91            for j in range(self.dimension):
 92                # For each location, the demand coverage z[i] should be less than or equal to
 93                # the total coverage provided by all edge computing nodes
 94                limit_z[i, j] = kw.core.quicksum(
 95                    self.coverage[f"{i + 1},{j + 1}", f"{i1 + 1},{j1 + 1}"]
 96                    * var_x[i1, j1]
 97                    for i1 in range(self.dimension)
 98                    for j1 in range(self.dimension)
 99                )
100
101        self.model = kw.core.QuboModel()
102        self.model.set_objective(-obj)
103        self.model.add_constraint(
104            var_x.sum() == self.num_nodes, "c1", penalty=self.penalty
105        )
106        self.model.add_constraint(
107            var_z <= limit_z, "c2", penalty=self.penalty, slack_var_expr=slack
108        )
109
110    def solve(self):
111        """Solving the QUBO model.
112
113        Returns:
114            tuple: Result dictionary and Result dictionary.
115
116            - dict: Result dictionary. The key is the variable name, and the value is the corresponding spin value.
117
118            - float: qubo value.
119        """
120        # Perform the Simulated Annealing algorithm
121        _solver = kw.classical.SimulatedAnnealingOptimizer(
122            initial_temperature=100000,
123            alpha=0.99,
124            cutoff_temperature=0.0001,
125            iterations_per_t=100,
126            rand_seed=10,
127        )
128        _sol_dict, _qubo_value = _solver.solve_qubo(self.model)
129        return _sol_dict, float(_qubo_value)
130
131    def recovery(self, sol_dict):
132        """Verify whether the solution is feasible"""
133        return self.model.verify_constraint(sol_dict)
134
135
136def assert_documented_results():
137    """Check that the documented model-building path stays valid."""
138    demand_data = {
139        "1,1": 38,
140        "1,2": 22,
141        "1,3": 65,
142        "1,4": 56,
143        "2,1": 53,
144        "2,2": 48,
145        "2,3": 76,
146        "2,4": 46,
147        "3,1": 56,
148        "3,2": 36,
149        "3,3": 7,
150        "3,4": 29,
151        "4,1": 50,
152        "4,2": 37,
153        "4,3": 48,
154        "4,4": 40,
155    }
156    solver = EdgeCoveragePlanner()
157    solver.prepare_data(demand_data)
158    solver.prepare_model()
159    assert solver.model is not None
160
161
162if __name__ == "__main__":
163    # Store computing power demand data in the DEM dictionary,
164    # where the keys are location coordinates and the values are computing power demands
165    # fmt: off
166    demand_data = {
167        '1,1': 38, '1,2': 22, '1,3': 65, '1,4': 56,
168        '2,1': 53, '2,2': 48, '2,3': 76, '2,4': 46,
169        '3,1': 56, '3,2': 36, '3,3': 7,  '3,4': 29,
170        '4,1': 50, '4,2': 37, '4,3': 48, '4,4': 40
171    }
172    # fmt: on
173    # Create an instance of the SPQCSolver class
174    solver = EdgeCoveragePlanner()
175
176    # Prepare data
177    solver.prepare_data(demand_data)
178
179    # Prepare the QUBO model with a specified penalty coefficient lambda
180    solver.prepare_model()
181
182    # Use the Simulated Annealing algorithm to find the optimal solution
183    best_sol_dict, qubo_value = solver.solve()
184
185    # Recover the original problem solution from the QUBO solution and check its feasibility
186    unsatisfied_count, result_dict = solver.recovery(best_sol_dict)
187    if unsatisfied_count == 0:
188        print("Find a feasible solution")
189        print("Objective value:", -qubo_value)
190    else:
191        print("No feasible solution")

Code for Question 2#

  1import math
  2from typing import Tuple
  3import numpy as np
  4import kaiwu as kw
  5from kaiwu.core import quicksum
  6
  7
  8class CloudEdgeUserSolver:
  9    """Solver for cloud-edge-user cost-minimization QUBO model."""
 10
 11    def __init__(self, edge_capacity: int = 12, edge_radius: float = 3.0) -> None:
 12        """Initialize core parameters and placeholders."""
 13        # capacities and radii
 14        self.edge_capacity = edge_capacity
 15        self.edge_radius = edge_radius
 16
 17        # Service node configurations
 18        self.user_locations = [
 19            "1,1",
 20            "1,4",
 21            "2,6",
 22            "3,3",
 23            "3,5",
 24            "4,4",
 25            "5,5",
 26            "6,3",
 27            "6,6",
 28        ]
 29        self.edge_locations = ["6,1", "2,3", "4,5", "6,5"]
 30        self.cloud_locations = ["4,0"]
 31
 32        # edge server costs
 33        self.fix_cost_edge = {"6,1": 60, "2,3": 42, "4,5": 46, "6,5": 54}
 34
 35        # variable costs
 36        self.var_cost_cloud = 1
 37        self.var_cost_edge = 2
 38
 39        # Unit transmission cost
 40        self.trans_cost_user_edge = 1
 41        self.trans_cost_user_cloud = 2
 42        self.trans_cost_edge_cloud = 1
 43
 44        # Demand data
 45        self.demand = {
 46            "1,1": 7,
 47            "1,4": 4,
 48            "2,6": 5,
 49            "3,3": 9,
 50            "3,5": 8,
 51            "4,4": 11,
 52            "5,5": 1,
 53            "6,3": 7,
 54            "6,6": 5,
 55        }
 56
 57        # Precomputed data
 58        self.num_user = 0
 59        self.num_edge = 0
 60        self.num_cloud = 0
 61        self.distances = {}
 62        self.coverage = {}
 63        self.qubo_model = None
 64
 65    def prepare_data(self):
 66        """Prepare data for QUBO model."""
 67        # initialize user demands
 68        # fmt: off
 69        loc_set_inner_full = ['1,1', '1,2', '1,3', '1,4', '1,5', '1,6',
 70                              '2,1', '2,2', '2,3', '2,4', '2,5', '2,6',
 71                              '3,1', '3,2', '3,3', '3,4', '3,5',
 72                              '4,1', '4,2', '4,3', '4,4', '4,5', '4,6',
 73                              '5,1', '5,2', '5,3', '5,4', '5,5', '5,6',
 74                              '6,1', '6,2', '6,3', '6,4', '6,5', '6,6']
 75        # fmt: on
 76        # all nodes
 77        loc_set = loc_set_inner_full + self.cloud_locations
 78
 79        self.num_cloud = len(self.cloud_locations)
 80        self.num_edge = len(self.edge_locations)
 81        self.num_user = len(self.user_locations)
 82
 83        # compute distances
 84        for i in loc_set:
 85            for j in loc_set:
 86                self.distances[(i, j)] = round(
 87                    np.sqrt(
 88                        (
 89                            int(i.split(",", maxsplit=1)[0])
 90                            - int(j.split(",", maxsplit=1)[0])
 91                        )
 92                        ** 2
 93                        + (
 94                            int(i.split(",", maxsplit=1)[1])
 95                            - int(j.split(",", maxsplit=1)[1])
 96                        )
 97                        ** 2
 98                    ),
 99                    2,
100                )
101
102        # compute coverage relationships
103        for i in loc_set:
104            for j in loc_set:
105                if self.distances[(i, j)] <= self.edge_radius:
106                    self.coverage[(i, j)] = 1
107                else:
108                    self.coverage[(i, j)] = 0
109
110    def prepare_model(
111        self,
112        eq_penalties: Tuple[float, float, float] = (1e4, 1e4, 1e4),
113        ineq_penalties: Tuple[float, float] = (1e4, 1e4),
114    ) -> None:
115        """
116        Build QUBO with 3 equality and 2 inequality constraints.
117        """
118        # compute the maximum demand for each edge
119        max_ujk = []
120        for j in self.edge_locations:
121            max_ujk.append(
122                sum(
123                    self.demand[i] * self.coverage[i, j] for i in self.user_locations
124                )
125            )
126
127        # initialize decision variables
128        x_edge = kw.core.ndarray(self.num_edge, "x_edge", kw.core.Binary)
129        y_ij = kw.core.ndarray((self.num_user, self.num_edge), "yij", kw.core.Binary)
130        y_jk = kw.core.ndarray((self.num_edge, self.num_cloud), "yjk", kw.core.Binary)
131        y_ik = kw.core.ndarray((self.num_user, self.num_cloud), "yik", kw.core.Binary)
132
133        # for each edge, if the maximum demand does not exceed the edge's capacity, set the corresponding yjk to 0
134        for j in range(self.num_edge):
135            if max_ujk[j] <= self.edge_capacity:
136                y_jk[j][0] = 0
137
138        # initialize ujk related variables
139        u_jk = np.zeros(
140            shape=(self.num_edge, self.num_cloud), dtype=kw.core.BinaryExpression
141        )
142        ujk_residual = np.zeros(shape=self.num_edge, dtype=kw.core.BinaryExpression)
143        for j in range(self.num_edge):
144            ujk_residual[j] = (
145                quicksum(
146                    self.demand[self.user_locations[i]] * y_ij[i][j]
147                    for i in range(self.num_user)
148                )
149                - self.edge_capacity
150            )
151            for k in range(self.num_cloud):
152                u_jk[j][k] = y_jk[j][k] * ujk_residual[j]
153        # build objective
154        obj = self._build_objective_components(u_jk, x_edge, y_ij, y_ik)
155        # build equality constraint
156        constraint1, constraint2, constraint3 = self._build_eq_constraint(
157            x_edge, y_ij, y_ik, y_jk
158        )
159        # build inequality constraint
160        ineq_qubo1, ineq_qubo2 = self._build_ineq_constraint(
161            max_ujk, ujk_residual, y_ij, y_jk
162        )
163
164        # building the final model
165        self.qubo_model = kw.core.QuboModel()
166        self.qubo_model.set_objective(obj)
167        self.qubo_model.add_constraint(constraint1, name="c1", penalty=eq_penalties[0])
168        self.qubo_model.add_constraint(constraint2, name="c2", penalty=eq_penalties[1])
169        self.qubo_model.add_constraint(constraint3, name="c3", penalty=eq_penalties[2])
170        self.qubo_model.add_constraint(ineq_qubo1, name="c4", penalty=ineq_penalties[0])
171        self.qubo_model.add_constraint(ineq_qubo2, name="c5", penalty=ineq_penalties[1])
172
173    def _build_ineq_constraint(self, max_ujk, ujk_residual, y_ij, y_jk):
174        # inequality constraint 1: after subtracting the edge's maximum capacity from demand,
175        # the yjk constraint should hold
176        ineq_constraint1 = []
177        ineq_qubo1 = kw.core.BinaryExpression(coefficient={}, offset=0)
178        len_slack1 = math.ceil(math.log2(max(max_ujk) + 1))
179        slack1 = kw.core.ndarray(
180            (self.num_edge, self.num_cloud, len_slack1), "slack1", kw.core.Binary
181        )
182        for j in range(self.num_edge):
183            ineq_constraint1.append([])
184            for k in range(self.num_cloud):
185                if y_jk[j][k] == 0:
186                    ineq_constraint1[j].append(0)
187                else:
188                    ineq_constraint1[j].append(
189                        ujk_residual[j] - (max_ujk[j] - self.edge_capacity) * y_jk[j][k]
190                    )
191                    ineq_qubo1 += (
192                        ineq_constraint1[j][k]
193                        + quicksum(slack1[j][k][_] * (2**_) for _ in range(len_slack1))
194                    ) ** 2
195        # inequality constraint 2: the capacity of an edge should be greater than or equal to the demand
196        ineq_qubo2 = kw.core.BinaryExpression(coefficient={}, offset=0)
197        ineq_constraint2 = []
198        len_slack2 = math.ceil(math.log2(max(max_ujk) + 1))
199        slack2 = kw.core.ndarray(
200            (self.num_edge, self.num_cloud, len_slack2), "slack2", kw.core.Binary
201        )
202        for j in range(self.num_edge):
203            ineq_constraint2.append([])
204            for k in range(self.num_cloud):
205                if y_jk[j][k] == 0:
206                    ineq_constraint2[j].append(0)
207                else:
208                    ineq_constraint2[j].append(
209                        y_jk[j][k] * self.edge_capacity
210                        - quicksum(
211                            self.demand[self.user_locations[i]] * y_ij[i][j]
212                            for i in range(self.num_user)
213                        )
214                    )
215                    ineq_qubo2 += (
216                        ineq_constraint2[j][k]
217                        + quicksum(slack2[j][k][_] * (2**_) for _ in range(len_slack2))
218                    ) ** 2
219        return ineq_qubo1, ineq_qubo2
220
221    def _build_eq_constraint(self, x_edge, y_ij, y_ik, y_jk):
222        # constraint 1: ensure that each user's service demand is assigned to only one location (either edge or cloud)
223        constraint1 = 0
224        for i in range(self.num_user):
225            constraint1 += (
226                quicksum(y_ij[i][j] for j in range(self.num_edge))
227                + quicksum(y_ik[i][k] for k in range(self.num_cloud))
228                - 1
229            ) ** 2
230        # constraint 2: initialize constraint expression
231        constraint2 = 0
232        for i in range(self.num_user):
233            for j in range(self.num_edge):
234                if self.coverage[(self.user_locations[i], self.edge_locations[j])] == 0:
235                    y_ij[i][j] = 0
236                else:
237                    constraint2 += y_ij[i][j] * (1 - x_edge[j])
238        # constraint 3: ensure the relationship between yjk and x_edge
239        constraint3 = 0
240        for j in range(self.num_edge):
241            for k in range(self.num_cloud):
242                constraint3 += y_jk[j][k] * (1 - x_edge[j])
243        return constraint1, constraint2, constraint3
244
245    def _build_objective_components(self, u_jk, x_edge, y_ij, y_ik):
246        # objective function
247        c_fix = quicksum(
248            self.fix_cost_edge[self.edge_locations[j]] * x_edge[j]
249            for j in range(self.num_edge)
250        )
251        c_var = self.var_cost_cloud * quicksum(
252            quicksum(
253                self.demand[self.user_locations[i]] * y_ik[i][k]
254                for i in range(self.num_user)
255            )
256            for k in range(self.num_cloud)
257        )
258        c_var += self.var_cost_edge * quicksum(
259            quicksum(
260                self.demand[self.user_locations[i]] * y_ij[i][j]
261                for i in range(self.num_user)
262            )
263            for j in range(self.num_edge)
264        )
265        c_var += (self.var_cost_cloud - self.var_cost_edge) * quicksum(
266            quicksum(u_jk[j][k] for j in range(self.num_edge))
267            for k in range(self.num_cloud)
268        )
269        c_tran = self.trans_cost_user_edge * quicksum(
270            quicksum(
271                self.demand[self.user_locations[i]]
272                * self.distances[(self.user_locations[i], self.edge_locations[j])]
273                * y_ij[i][j]
274                for i in range(self.num_user)
275            )
276            for j in range(self.num_edge)
277        )
278        c_tran += self.trans_cost_user_cloud * quicksum(
279            quicksum(
280                self.demand[self.user_locations[i]]
281                * self.distances[(self.user_locations[i], self.cloud_locations[k])]
282                * y_ik[i][k]
283                for i in range(self.num_user)
284            )
285            for k in range(self.num_cloud)
286        )
287        c_tran += self.trans_cost_edge_cloud * quicksum(
288            quicksum(
289                self.distances[(self.edge_locations[j], self.cloud_locations[k])]
290                * u_jk[j][k]
291                for j in range(self.num_edge)
292            )
293            for k in range(self.num_cloud)
294        )
295        return c_fix + c_tran + c_var
296
297    def solve(
298        self,
299        max_iterations: int = 10,
300        init_temp: float = 1e3,
301        decay: float = 0.99,
302        min_temp: float = 1e-4,
303        iter_per_temp: int = 10,
304    ):
305        """
306        Perform simulated annealing.
307        """
308        best = math.inf
309        for num in range(max_iterations):
310            solver = kw.classical.SimulatedAnnealingOptimizer(
311                initial_temperature=init_temp,
312                alpha=decay,
313                cutoff_temperature=min_temp,
314                iterations_per_t=iter_per_temp,
315            )
316            sol, val = solver.solve_qubo(self.qubo_model)
317            feasible = self.recovery(sol)
318            if feasible and val < best:
319                best = val
320                print(f"Iter {num}: best={val}")
321        print(f"Optimal={best}")
322
323    def recovery(self, sol_dict: dict) -> bool:
324        """
325        Validate solution via kw.core.get_val interface.
326        """
327        unsatisfied_count, _ = self.qubo_model.verify_constraint(sol_dict)
328        return not unsatisfied_count
329
330
331def assert_documented_results():
332    """Check that the documented model-building path stays valid."""
333    solver = CloudEdgeUserSolver()
334    solver.prepare_data()
335    solver.prepare_model()
336    assert solver.qubo_model is not None
337
338
339if __name__ == "__main__":
340    # create an instance of the solver class
341    solver = CloudEdgeUserSolver()
342
343    # prepare the data
344    solver.prepare_data()
345
346    # prepare the qubo model, set the penalty coefficient lambda
347    solver.prepare_model()
348
349    # use simulated annealing to find the optimal solution
350    solver.solve()