-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Satellite scheduling app #260
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
513d6eb
Code clean-up refactoring in LCA module
srrisbud 5ca5fc6
Merge branch 'lava-nc:main' into main
srrisbud 7cb6706
Merge branch 'lava-nc:main' into main
srrisbud 8e1acf9
Merge branch 'lava-nc:main' into main
srrisbud 7533982
Merge branch 'lava-nc:main' into main
srrisbud d5f9c15
Merge branch 'lava-nc:main' into main
srrisbud 02e843a
First working commit of Scheduler
srrisbud 05cf301
Merge branch 'main' into refactor_ss
srrisbud 276a231
Merge branch 'lava-nc:main' into main
srrisbud 095cd05
Merge branch 'main' into refactor_ss
srrisbud 4d827aa
Scheduler and SatelliteScheduler work as intended
srrisbud 5b097be
Minor change to NEBM Process
srrisbud bc00f59
Added toggles for profiling to the Scheduling Solver
srrisbud 5a76a55
Some tests
srrisbud cd4bcc5
Merge branch 'lava-nc:main' into main
srrisbud 1003e82
Merge branch 'main' into refactor_ss
srrisbud e0f6b26
Merge branch 'lava-nc:main' into main
srrisbud 7344a45
Expose the generated Q-matrix to user through a property
srrisbud c4dce21
Minor changes to a scratch test
srrisbud fad00e9
Merge branch 'main' into refactor_ss
srrisbud dc28026
Scheduler unittests
srrisbud bc0b6f7
Delinting
srrisbud 02e5987
Removed the legacy SatScheduler
srrisbud 3b4b5e0
Merge branch 'lava-nc:main' into main
srrisbud 2b985c5
Merge branch 'main' into refactor_ss
srrisbud d6a7d98
Merge branch 'main' into refactor_ss
srrisbud 01d1cab
Merge branch 'refactor_ss' of github.com:srrisbud/lava-optimization i…
srrisbud 5bd4f7b
Post-code-review suggestions by @phstratmann incorporated
srrisbud fdb7c04
Minor parameter redefinitions in the SatelliteScheduler
srrisbud d320827
Minor image fix in sat scheduling jupyter notebook
srrisbud b0b4901
Fixed minor errors
srrisbud File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,329 @@ | ||
# Copyright (C) 2023 Intel Corporation | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
# See: https://spdx.org/licenses/ | ||
|
||
|
||
from typing import Optional, Union | ||
import numpy as np | ||
import networkx as ntx | ||
|
||
import matplotlib.pyplot as plt | ||
from matplotlib.patches import PathPatch | ||
from matplotlib.path import Path | ||
|
||
|
||
class SchedulingProblem: | ||
def __init__(self, | ||
num_agents: int = 3, | ||
num_tasks: int = 3, | ||
sat_cutoff: Union[float, int] = 0.99, | ||
seed: int = 42): | ||
"""Schedule `num_tasks` tasks among `num_agents` agents such that | ||
every agent performs exactly one task and every task gets assigned | ||
to exactly one agent. | ||
|
||
Parameters | ||
---------- | ||
num_agents (int) : number of agents available to perform all tasks. | ||
Default is arbitrarily chosen as 3. | ||
|
||
num_tasks (int) : number of tasks to be performed. Default is | ||
arbitrarily chosen as 3. | ||
|
||
sat_cutoff (float or int) : If provided as a float, it is interpreted | ||
as satisfiability cut-off, which is the ratio between the number | ||
of tasks for which an agent gets assigned to the total number of | ||
tasks. Needs to be a fraction between 0 and 1 in this case. | ||
If provided as an int, this is the target cost for the underlying QUBO | ||
solver. Default is 0.99 (i.e., 99% of the total number of tasks get | ||
assigned an agent). | ||
|
||
seed (int) : Seed for PRNG used in problem generation. | ||
""" | ||
self._num_agents = num_agents | ||
self._agent_ids = range(num_agents) | ||
self._agent_attrs = None | ||
self._num_tasks = num_tasks | ||
self._task_ids = range(num_tasks) | ||
self._task_attrs = None | ||
self._sat_cutoff = sat_cutoff | ||
self.graph = None | ||
self.adjacency = None | ||
self._random_seed = seed | ||
|
||
@property | ||
def num_agents(self): | ||
return self._num_agents | ||
|
||
@num_agents.setter | ||
def num_agents(self, val: int): | ||
self._num_agents = val | ||
|
||
@property | ||
def agent_ids(self): | ||
return self._agent_ids | ||
|
||
@property | ||
def agent_attrs(self): | ||
return self._agent_attrs | ||
|
||
@agent_attrs.setter | ||
def agent_attrs(self, attr_vec): | ||
self._agent_attrs = attr_vec | ||
|
||
@property | ||
def num_tasks(self): | ||
return self._num_tasks | ||
|
||
@num_tasks.setter | ||
def num_tasks(self, val: int): | ||
self._num_tasks = val | ||
|
||
@property | ||
def task_ids(self): | ||
return self._task_ids | ||
|
||
@property | ||
def task_attrs(self): | ||
return self._task_attrs | ||
|
||
@task_attrs.setter | ||
def task_attrs(self, attr_vec): | ||
self._task_attrs = attr_vec | ||
|
||
@property | ||
def sat_cutoff(self): | ||
return self._sat_cutoff | ||
|
||
@sat_cutoff.setter | ||
def sat_cutoff(self, val: float): | ||
self._sat_cutoff = val | ||
|
||
@property | ||
def random_seed(self): | ||
return self._random_seed | ||
|
||
@random_seed.setter | ||
def random_seed(self, val: int): | ||
self._random_seed = val | ||
|
||
def is_node_valid(self, *args): | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Checks if a node is valid to be included in the problem graph. | ||
|
||
Over-ridden by derived child classes to suit their purpose. The base | ||
class method always returns True, indicating that all nodes are valid | ||
in the case of a base Scheduling Problem. | ||
""" | ||
return True | ||
|
||
def is_edge_conflicting(self, node1, node2): | ||
nodes = self.graph.nodes | ||
is_same_agent = (nodes[node1]["agent_id"] == nodes[node2]["agent_id"]) | ||
is_same_task = (nodes[node1]["task_id"] == nodes[node2]["task_id"]) | ||
return True if is_same_agent or is_same_task else False | ||
|
||
def generate(self, seed=None): | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" Generate a new scheduler problem. """ | ||
if self.random_seed: | ||
np.random.seed(self.random_seed) | ||
if not self.random_seed or seed != self.random_seed: | ||
# set seed only if it's different | ||
self.random_seed = seed | ||
np.random.seed(seed) | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.graph = ntx.Graph() | ||
self._generate_valid_nodes() | ||
self._generate_edges_from_constraints() | ||
self._rescale_adjacency() | ||
|
||
def _generate_valid_nodes(self): | ||
"""Generate nodes and check if they are valid before adding them to | ||
the problem graph. | ||
""" | ||
node_id = 0 | ||
if self.agent_attrs is None: | ||
self.agent_attrs = np.reshape(self.agent_ids, | ||
(len(self.agent_ids), 1)) | ||
agent_id_attr_map = dict(zip(self.agent_ids, self.agent_attrs)) | ||
if self.task_attrs is None: | ||
self.task_attrs = ( | ||
np.tile(np.reshape(self.task_ids, | ||
(len(self.task_ids), 1)), (1, 2))) | ||
task_id_attr_map = dict(zip(self.task_ids, self.task_attrs)) | ||
for aid, a_attr in agent_id_attr_map.items(): # for all agents | ||
for tid, t_attr in task_id_attr_map.items(): # for all tasks | ||
# Check if (agent, task) is a valid node | ||
if self.is_node_valid(aid, tid): | ||
# If it is, add it to the problem graph | ||
self.graph.add_node(node_id, | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
agent_id=aid, | ||
task_id=tid, | ||
agent_attr=a_attr, | ||
task_attr=t_attr) | ||
node_id += 1 | ||
|
||
def _generate_edges_from_constraints(self): | ||
num_nodes = len(self.graph.nodes) | ||
self.adjacency = ( | ||
np.zeros((num_nodes, num_nodes), dtype=int)) | ||
for n1 in self.graph.nodes: | ||
for n2 in self.graph.nodes: | ||
not_same = n1 != n2 | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
is_conflict = self.is_edge_conflicting(n1, n2) | ||
if not_same and is_conflict: | ||
self.graph.add_edge(n1, n2) | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.adjacency[n1, n2] = 1 | ||
|
||
def _rescale_adjacency(self): | ||
""" Scale the adjacency matrix weights for QUBO solver. """ | ||
self.adjacency = np.triu(self.adjacency) | ||
self.adjacency += self.adjacency.T - 2 * np.diag( | ||
self.adjacency.diagonal()) | ||
|
||
|
||
class SatelliteScheduleProblem(SchedulingProblem): | ||
""" | ||
SatelliteScheduleProblem is a synthetic scheduling problem in which a | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
number of vehicles must be assigned to view as many requests in a | ||
2-dimensional plane as possible. Each vehicle moves horizontally across | ||
the plane, has minimum and maximum view angle, and has a maximum rotation | ||
rate (i.e. the rate at which the vehicle can reorient vertically from one | ||
target to the next). | ||
|
||
The problem is represented as an infeasibility graph and can be solved by | ||
finding the Maximum Independent Set. | ||
|
||
Parameters | ||
---------- | ||
num_satellites : int, default = 6 | ||
The number of satellites to generate schedules for. | ||
view_height : float, default = 0.25 | ||
The range from minimum to maximum viewable angle for each satellite. | ||
view_coords : Optional[np.ndarray], default = None | ||
The view coordinates (i.e. minimum viewable angle) for each | ||
satellite in a numpy array. If None, view coordinates will be | ||
evenly distributed across the viewable range. | ||
num_requests : int, default = 48 | ||
The number of requests to generate. | ||
turn_rate : float, default = 2 | ||
How quickly each satellite may reorient its view angle. | ||
solution_criteria : float, default = 0.99 | ||
The target for a successful solution. The solver will stop | ||
looking for a better schedule if the specified fraction of | ||
requests is satisfied. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
num_satellites: int = 6, | ||
view_height: float = 0.25, | ||
view_coords: Optional[np.ndarray] = None, | ||
num_requests: int = 48, | ||
requests: Optional[np.ndarray] = None, | ||
turn_rate: float = 2, | ||
solution_criteria: float = 0.99, | ||
seed: int = 42, | ||
): | ||
""" Create a SatelliteScheduleProblem. | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
super(SatelliteScheduleProblem, | ||
self).__init__(num_agents=num_satellites, | ||
num_tasks=num_requests, | ||
sat_cutoff=solution_criteria, | ||
seed=seed) | ||
self.num_satellites = self.num_agents | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.num_requests = self.num_tasks | ||
|
||
self.view_height = view_height * (1 / (num_satellites - 1)) | ||
if view_coords is None: | ||
self.view_coords = np.linspace(0, | ||
1, | ||
num_satellites) | ||
else: | ||
self.view_coords = view_coords | ||
self.agent_attrs = list(zip([self.view_height] * num_satellites, | ||
self.view_coords)) | ||
self.satellites = self.agent_ids | ||
self.turn_rate = turn_rate | ||
self.requests = None | ||
self.qubo_problem = None | ||
self.generate_requests(requests) | ||
self.request_density = self.requests.shape[0] / (1 + self.view_height) | ||
|
||
def generate_requests(self, requests=None) -> None: | ||
""" Generate a random set of requests in the 2D plane. """ | ||
if requests is not None: | ||
self.requests = requests | ||
else: | ||
np.random.seed(self.random_seed) | ||
self.requests = np.random.random((self.num_requests, 2)) | ||
self.requests[:, 1] = (1 + self.view_height) * ( | ||
self.requests[:, 1]) - (self.view_height / 2) | ||
order = np.argsort(self.requests[:, 0]) | ||
self.requests = self.requests[order, :] | ||
self.task_attrs = self.requests.tolist() | ||
|
||
def is_node_valid(self, sat_id, req_id): | ||
""" Return whether the request is visible to the satellite. """ | ||
view_height = self.agent_attrs[sat_id][0] | ||
satellite_y_coord = self.agent_attrs[sat_id][1] | ||
request_y_coord = self.task_attrs[req_id][1] | ||
lower_bound = satellite_y_coord - view_height / 2 | ||
upper_bound = satellite_y_coord + view_height / 2 | ||
return lower_bound <= request_y_coord <= upper_bound | ||
|
||
def is_req_reachable(self, n1, n2): | ||
nodes = self.graph.nodes | ||
n1_req_coords = nodes[n1]["task_attr"] | ||
n2_req_coords = nodes[n2]["task_attr"] | ||
delta_x = abs(n1_req_coords[0] - n2_req_coords[0]) | ||
delta_y = abs(n1_req_coords[1] - n2_req_coords[1]) | ||
return self.turn_rate * delta_x >= delta_y | ||
|
||
def is_edge_conflicting(self, node1, node2): | ||
srrisbud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
nodes = self.graph.nodes | ||
is_same_satellite = (nodes[node1]["agent_id"] == nodes[node2][ | ||
"agent_id"]) | ||
is_same_request = (nodes[node1]["task_id"] == nodes[node2]["task_id"]) | ||
return is_same_request or (is_same_satellite and not | ||
self.is_req_reachable(node1, node2)) | ||
|
||
def plot_problem(self): | ||
""" Plot the problem state using pyplot. """ | ||
plt.figure(figsize=(12, 4), dpi=120) | ||
plt.subplot(131) | ||
plt.scatter(self.requests[:, 0], | ||
self.requests[:, 1], | ||
s=2) | ||
for y in self.view_coords: | ||
codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] | ||
verts = [[-0.05, y], | ||
[0.05, y + self.view_height / 2], | ||
[0.05, y - self.view_height / 2], | ||
[-0.05, y]] | ||
plt.gca().add_patch( | ||
PathPatch(Path(verts, codes), ec='none', alpha=0.3, | ||
fc='lightblue')) | ||
plt.scatter([-0.05], [y], # + self.view_height / 2 | ||
s=10, marker='s', c='gray') | ||
plt.plot([0, 1], | ||
[y, # + self.view_height / 2 | ||
y], # + self.view_height / 2], | ||
'C1--', lw=0.75) | ||
plt.xticks([]) | ||
plt.yticks([]) | ||
plt.title( | ||
f'Schedule {self.num_satellites} satellites to observe ' | ||
f'{self.num_requests} targets.') | ||
plt.subplot(132) | ||
ntx.draw_networkx(self.graph, with_labels=False, | ||
node_size=2, width=0.5) | ||
plt.title( | ||
f'Infeasibility graph with {self.graph.number_of_nodes()} nodes.') | ||
plt.subplot(133) | ||
plt.imshow(self.adjacency, aspect='auto') | ||
plt.title( | ||
f'Adjacency matrix has {self.adjacency.mean():.2%} ' | ||
f'connectivity.') | ||
plt.yticks([]) | ||
plt.tight_layout() | ||
plt.show() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Beautiful code, following all suggestions I know of. Complete docstrings, properties, getter/setter...
Thanks for keeping the code quality high!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only minor point of criticism: You may want to add input validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will accrue technical debt on this one. Not worth the available time to add input validation everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it is reasonable that we will ever do it? If so, please add an issue to Github. If not, just ignore.