diff --git a/setup.py b/setup.py index b39ef45..f2d0405 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup(name='stn', - packages=['stn', 'stn.config', 'stn.methods', 'stn.pstn', 'stn.stnu', 'stn.utils'], + packages=['stn', 'stn.config', 'stn.exceptions', 'stn.methods', 'stn.pstn', 'stn.stnu', 'stn.utils'], version='0.2.0', install_requires=[ 'numpy', diff --git a/stn/config/config.py b/stn/config/config.py index 6991859..8ccaa29 100644 --- a/stn/config/config.py +++ b/stn/config/config.py @@ -82,7 +82,9 @@ def srea_algorithm(stn): return risk_metric, dispatchable_graph = result - return risk_metric, dispatchable_graph + dispatchable_graph.risk_metric = risk_metric + + return dispatchable_graph class DegreeStongControllability(object): @@ -108,14 +110,16 @@ def dsc_lp_algorithm(stn): stnu = dsc_lp.get_stnu(bounds) - # Returns a schedule because it is an offline approach + # The dispatchable graph is a schedule because it is an offline approach schedule = dsc_lp.get_schedule(bounds) # A strongly controllable STNU has a DSC of 1, i.e., a DSC value of 1 is better. We take # 1 − DC to be the risk metric, so that small values are preferable risk_metric = 1 - dsc - return risk_metric, schedule + schedule.risk_metric = risk_metric + + return schedule class FullPathConsistency(object): @@ -134,15 +138,19 @@ def fpc_algorithm(stn): if dispatchable_graph is None: return risk_metric = 1 - return risk_metric, dispatchable_graph + + dispatchable_graph.risk_metric = risk_metric + + return dispatchable_graph stn_factory = STNFactory() stn_factory.register_stn('fpc', STN) stn_factory.register_stn('srea', PSTN) -stn_factory.register_stn('dsc_lp', STNU) +stn_factory.register_stn('dsc', STNU) stp_solver_factory = STPSolverFactory() stp_solver_factory.register_solver('fpc', FullPathConsistency) stp_solver_factory.register_solver('srea', StaticRobustExecution) -stp_solver_factory.register_solver('dsc_lp', DegreeStongControllability) \ No newline at end of file +stp_solver_factory.register_solver('drea', StaticRobustExecution) +stp_solver_factory.register_solver('dsc', DegreeStongControllability) \ No newline at end of file diff --git a/stn/exceptions/__init__.py b/stn/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stn/exceptions/stp.py b/stn/exceptions/stp.py new file mode 100644 index 0000000..cc07fe5 --- /dev/null +++ b/stn/exceptions/stp.py @@ -0,0 +1,6 @@ +class NoSTPSolution(Exception): + + def __init__(self): + """ Raised when the stp solver cannot produce a solution for the problem + """ + Exception.__init__(self) \ No newline at end of file diff --git a/stn/methods/dsc_lp.py b/stn/methods/dsc_lp.py index 3bf0d38..09d8225 100644 --- a/stn/methods/dsc_lp.py +++ b/stn/methods/dsc_lp.py @@ -22,6 +22,7 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import copy import pulp import sys @@ -43,7 +44,7 @@ class DSC_LP(object): logger = logging.getLogger('stn.dsc_lp') def __init__(self, stnu): - self.stnu = stnu + self.stnu = copy.deepcopy(stnu) self.constraints = stnu.get_constraints() self.contingent_constraints = stnu.get_contingent_constraints() self.contingent_timepoints = stnu.get_contingent_timepoints() @@ -215,6 +216,8 @@ def compute_dsc(self, original, shrinked): return the value of degree of strong controllability """ + new = 1 + orig = 1 for i in range(len(original)): x, y = original[i] orig = y-x diff --git a/stn/methods/fpc.py b/stn/methods/fpc.py index 45c7370..f91638b 100644 --- a/stn/methods/fpc.py +++ b/stn/methods/fpc.py @@ -17,4 +17,4 @@ def get_minimal_network(stn): minimal_network.update_edges(shortest_path_array) return minimal_network else: - logger.warning("The minimal network is inconsistent") + logger.debug("The minimal network is inconsistent. STP could not be solved") diff --git a/stn/methods/srea.py b/stn/methods/srea.py index 45274b6..3b9e331 100644 --- a/stn/methods/srea.py +++ b/stn/methods/srea.py @@ -267,7 +267,13 @@ def srea_LP(inputstn, prob.writeLP('STN.lp') pulp.LpSolverDefault.msg = 10 - prob.solve() + # Based on https://stackoverflow.com/questions/27406858/pulp-solver-error + # Sometimes pulp throws an exception instead of returning a problem with unfeasible status + try: + prob.solve() + except pulp.PulpSolverError: + print("Problem unfeasible") + return None status = pulp.LpStatus[prob.status] if debug: diff --git a/stn/node.py b/stn/node.py index 4bf57d6..590c4d7 100644 --- a/stn/node.py +++ b/stn/node.py @@ -1,46 +1,61 @@ -from stn.utils.uuid import generate_uuid +from stn.utils.uuid import from_str class Node(object): """Represents a timepoint in the STN """ - def __init__(self, task_id, pose, node_type): + def __init__(self, task_id, node_type, is_executed=False, **kwargs): # id of the task represented by this node + if isinstance(task_id, str): + task_id = from_str(task_id) self.task_id = task_id - # Pose in the map where the node has to be executed - self.pose = pose - # The node can be of node_type zero_timepoint, navigation, start or finish + # The node can be of node_type zero_timepoint, start, pickup or delivery self.node_type = node_type + self.is_executed = is_executed + self.action_id = kwargs.get("action_id") def __str__(self): to_print = "" - to_print += "node {} {}".format(self.task_id, self.node_type) + to_print += "{} {} ".format(self.task_id, self.node_type) return to_print def __repr__(self): return str(self.to_dict()) def __hash__(self): - return hash((self.task_id, self.pose, self.node_type)) + return hash((self.task_id, self.node_type, self.is_executed)) def __eq__(self, other): if other is None: return False return (self.task_id == other.task_id and - self.pose == other.pose and - self.node_type == other.node_type) + self.node_type == other.node_type and + self.is_executed == other.is_executed and + self.action_id == other.action_id) + + def __ne__(self, other): + return not self.__eq__(other) + + def execute(self): + self.is_executed = True def to_dict(self): node_dict = dict() - node_dict['task_id'] = self.task_id - node_dict['pose'] = self.pose + node_dict['task_id'] = str(self.task_id) node_dict['node_type'] = self.node_type + node_dict['is_executed'] = self.is_executed + if self.action_id: + node_dict['action_id'] = str(self.action_id) return node_dict @staticmethod def from_dict(node_dict): task_id = node_dict['task_id'] - pose = node_dict['pose'] + if isinstance(task_id, str): + task_id = from_str(task_id) node_type = node_dict['node_type'] - node = Node(task_id, pose, node_type) + is_executed = node_dict.get('is_executed', False) + node = Node(task_id, node_type, is_executed) + if node_dict.get('action_id'): + node.action_id = from_str(node_dict['action_id']) return node diff --git a/stn/pstn/pstn.py b/stn/pstn/pstn.py index 591be60..94ad9bf 100644 --- a/stn/pstn/pstn.py +++ b/stn/pstn/pstn.py @@ -22,11 +22,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import logging +from json import JSONEncoder + from stn.pstn.constraint import Constraint from stn.stn import STN -from stn.stn import Node -from json import JSONEncoder -import logging +from stn.task import Timepoint class MyEncoder(JSONEncoder): @@ -48,17 +49,23 @@ def __str__(self): if self.has_edge(j, i) and i < j: # Constraints with the zero timepoint if i == 0: - timepoint = Node.from_dict(self.node[j]['data']) + timepoint = self.nodes[j]['data'] lower_bound = -self[j][i]['weight'] upper_bound = self[i][j]['weight'] to_print += "Timepoint {}: [{}, {}]".format(timepoint, lower_bound, upper_bound) + if timepoint.is_executed: + to_print += " Ex" # Constraints between the other timepoints else: if 'is_contingent' in self[j][i]: to_print += "Constraint {} => {}: [{}, {}] ({})".format(i, j, -self[j][i]['weight'], self[i][j]['weight'], self[i][j]['distribution']) + if self[i][j]['is_executed']: + to_print += " Ex" else: to_print += "Constraint {} => {}: [{}, {}]".format(i, j, -self[j][i]['weight'], self[i][j]['weight']) + if self[i][j]['is_executed']: + to_print += " Ex" to_print += "\n" @@ -89,27 +96,8 @@ def add_constraint(self, i, j, wji=0.0, wij=float('inf'), distribution=""): super().add_constraint(i, j, wji, wij) - self.add_edge(i, j, distribution=distribution) - self.add_edge(i, j, is_contingent=is_contingent) - - self.add_edge(j, i, distribution=distribution) - self.add_edge(j, i, is_contingent=is_contingent) - - def timepoint_hard_constraints(self, node_id, task, node_type): - """ Adds the earliest and latest times to execute a timepoint (node) - Navigation timepoint [0, inf] - Start timepoint [earliest_start_time, latest_start_time] - Finish timepoint [0, inf] - """ - - if node_type == "navigation": - self.add_constraint(0, node_id, task.r_earliest_navigation_start_time) - - if node_type == "start": - self.add_constraint(0, node_id, task.r_earliest_start_time, task.r_latest_start_time) - - elif node_type == "finish": - self.add_constraint(0, node_id) + self.add_edge(i, j, distribution=distribution, is_contingent=is_contingent) + self.add_edge(j, i, distribution=distribution, is_contingent=is_contingent) def get_contingent_constraints(self): """ Returns a dictionary with the contingent constraints in the PSTN @@ -126,9 +114,9 @@ def get_contingent_constraints(self): def add_intertimepoints_constraints(self, constraints, task): """ Adds constraints between the timepoints of a task Constraints between: - - navigation start and start (contingent) - - start and finish (contingent) - - finish and next task (if any) (requirement) + - start and pickup (contingent) + - pickup and delivery (contingent) + - delivery and next task (if any) (requirement) Args: constraints (list) : list of tuples that defines the pair of nodes between which a new constraint should be added Example: @@ -139,28 +127,43 @@ def add_intertimepoints_constraints(self, constraints, task): """ for (i, j) in constraints: self.logger.debug("Adding constraint: %s ", (i, j)) - if self.node[i]['data']['node_type'] == "navigation": - distribution = self.get_navigation_distribution(i, j) - self.add_constraint(i, j, distribution=distribution) + if self.nodes[i]['data'].node_type == "start": + distribution = self.get_travel_time_distribution(task) + if distribution.endswith("_0.0"): # the distribution has no variation (stdev is 0) + # Make the constraint a requirement constraint + mean = float(distribution.split("_")[1]) + self.add_constraint(i, j, mean, mean) + else: + self.add_constraint(i, j, distribution=distribution) - elif self.node[i]['data']['node_type'] == "start": - distribution = self.get_task_distribution(task) + elif self.nodes[i]['data'].node_type == "pickup": + distribution = self.get_work_time_distribution(task) self.add_constraint(i, j, distribution=distribution) - elif self.node[i]['data']['node_type'] == "finish": + elif self.nodes[i]['data'].node_type == "delivery": # wait time between finish of one task and start of the next one. Fixed to [0, inf] self.add_constraint(i, j) - def get_navigation_distribution(self, source, destination): - """ Reads from the database the probability distribution for navigating from source to destination - """ - # TODO: Read estimated distribution from dataset - distribution = "N_1_1" - return distribution - - def get_task_distribution(self, task): - """ Reads from the database the estimated distribution of the task - In the case of transportation tasks, the estimated distribution is the navigation time from the pickup to the delivery location - """ - distribution = "N_1_1" - return distribution + @staticmethod + def get_travel_time_distribution(task): + travel_time = task.get_edge("travel_time") + travel_time_distribution = "N_" + str(travel_time.mean) + "_" + str(travel_time.standard_dev) + return travel_time_distribution + + @staticmethod + def get_work_time_distribution(task): + work_time = task.get_edge("work_time") + work_time_distribution = "N_" + str(work_time.mean) + "_" + str(work_time.standard_dev) + return work_time_distribution + + @staticmethod + def get_prev_timepoint(timepoint_name, next_timepoint, edge_in_between): + r_earliest_time = 0 + r_latest_time = float('inf') + return Timepoint(timepoint_name, r_earliest_time, r_latest_time) + + @staticmethod + def get_next_timepoint(timepoint_name, prev_timepoint, edge_in_between): + r_earliest_time = 0 + r_latest_time = float('inf') + return Timepoint(timepoint_name, r_earliest_time, r_latest_time) diff --git a/stn/stn.py b/stn/stn.py index 3420b75..cf83f44 100644 --- a/stn/stn.py +++ b/stn/stn.py @@ -9,6 +9,9 @@ from stn.node import Node from uuid import UUID +import copy +import math +from stn.task import Timepoint MAX_FLOAT = sys.float_info.max @@ -30,6 +33,7 @@ def __init__(self): super().__init__() self.add_zero_timepoint() self.max_makespan = MAX_FLOAT + self.risk_metric = None def __str__(self): to_print = "" @@ -37,21 +41,59 @@ def __str__(self): if self.has_edge(j, i) and i < j: # Constraints with the zero timepoint if i == 0: - timepoint = Node.from_dict(self.node[j]['data']) + timepoint = self.nodes[j]['data'] lower_bound = -self[j][i]['weight'] upper_bound = self[i][j]['weight'] to_print += "Timepoint {}: [{}, {}]".format(timepoint, lower_bound, upper_bound) + if timepoint.is_executed: + to_print += " Ex" # Constraints between the other timepoints else: to_print += "Constraint {} => {}: [{}, {}]".format(i, j, -self[j][i]['weight'], self[i][j]['weight']) + if self[i][j]['is_executed']: + to_print += " Ex" to_print += "\n" return to_print + def __eq__(self, other): + if other is None: + return False + if len(other.nodes()) != len(self.nodes()): + return False + for (i, j, data) in self.edges.data(): + if other.has_edge(i, j): + if other[i][j]['weight'] != self[i][j]['weight']: + return False + else: + return False + if other.has_node(i): + if other.nodes[i]['data'] != self.nodes[i]['data'] : + return False + else: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + def add_zero_timepoint(self): - node = Node(generate_uuid(), '', 'zero_timepoint') - self.add_node(0, data=node.to_dict()) + node = Node(generate_uuid(), 'zero_timepoint') + self.add_node(0, data=node) + + def get_earliest_time(self): + edges = [e for e in self.edges] + first_edge = edges[0] + return -self[first_edge[1]][0]['weight'] + + def get_latest_time(self): + edges = [e for e in self.edges] + last_edge = edges[-1] + return self[0][last_edge[0]]['weight'] + + def is_empty(self): + return nx.is_empty(self) def add_constraint(self, i, j, wji=0.0, wij=float('inf')): """ @@ -75,8 +117,8 @@ def add_constraint(self, i, j, wji=0.0, wij=float('inf')): # Maximum allocated time between i and j max_time = wij - self.add_edge(j, i, weight=min_time) - self.add_edge(i, j, weight=max_time) + self.add_edge(j, i, weight=min_time, is_executed=False) + self.add_edge(i, j, weight=max_time, is_executed=False) def remove_constraint(self, i, j): """ i : starting node id @@ -99,89 +141,74 @@ def get_constraints(self): return constraints - def get_node_pose(self, task, node_type): - """ Returns the pose in the map where the task has to be executed - """ - if node_type == 'navigation': - # TODO: initialize the pose with the robot current position (read it from mongo) - # this value will be overwritten once the task is allocated - pose = '' - elif node_type == 'start': - pose = task.start_pose_name - elif node_type == 'finish': - pose = task.finish_pose_name - - return pose - - def add_timepoint(self, id, task, node_type): + def add_timepoint(self, id, task, node_type, **kwargs): """ A timepoint is represented by a node in the STN The node can be of node_type: - zero_timepoint: references the schedule to the origin - - navigation: time at which the robot starts navigating towards the task - - start: time at which the robot starts executing the task - - finish: time at which the robot finishes executing the task + - start : time at which the robot starts navigating towards the pickup location + - pickup : time at which the robot arrives starts the pickup action + - delivery : time at which the robot finishes the delivery action """ - pose = self.get_node_pose(task, node_type) - node = Node(task.id, pose, node_type) - self.add_node(id, data=node.to_dict()) + node = Node(task.task_id, node_type, **kwargs) + self.add_node(id, data=node) def add_task(self, task, position=1): """ A task is added as 3 timepoints and 5 constraints in the STN" Timepoints: - - navigation start - - start time - - finish time + - start + - pickup time + - delivery time Constraints: - - earliest and latest navigation times - - navigation duration - earliest and latest start times - - task duration + - travel time: time to go from current position to pickup position) + - earliest and latest pickup times + - work time: time to perform the task (time to transport an object from the pickup to the delivery location) - earliest and latest finish times If the task is not the first in the STN, add wait time constraint Note: Position 0 is reserved for the zero_timepoint Add tasks from postion 1 onwards """ - self.logger.info("Adding task %s in position %s", task.id, position) + self.logger.info("Adding task %s in position %s", task.task_id, position) - navigation_node_id = 2 * position + (position-2) - start_node_id = navigation_node_id + 1 - finish_node_id = start_node_id + 1 + start_node_id = 2 * position + (position-2) + pickup_node_id = start_node_id + 1 + delivery_node_id = pickup_node_id + 1 - # Remove constraint linking navigation_node_id and previous node (if any) - if self.has_edge(navigation_node_id-1, navigation_node_id) and navigation_node_id-1 != 0: - self.logger.debug("Deleting constraint: %s => %s", navigation_node_id-1, navigation_node_id) + # Remove constraint linking start_node_id and previous node (if any) + if self.has_edge(start_node_id-1, start_node_id) and start_node_id-1 != 0: + self.logger.debug("Deleting constraint: %s => %s", start_node_id-1, start_node_id) - self.remove_constraint(navigation_node_id-1, navigation_node_id) + self.remove_constraint(start_node_id-1, start_node_id) # Displace by 3 all nodes and constraints after position mapping = {} for node_id, data in self.nodes(data=True): - if node_id >= navigation_node_id: + if node_id >= start_node_id: mapping[node_id] = node_id + 3 self.logger.debug("mapping: %s ", mapping) nx.relabel_nodes(self, mapping, copy=False) # Add new timepoints - self.add_timepoint(navigation_node_id, task, "navigation") - self.add_timepoint_constraints(navigation_node_id, task, "navigation") - self.add_timepoint(start_node_id, task, "start") - self.add_timepoint_constraints(start_node_id, task, "start") + self.add_timepoint_constraint(start_node_id, task.get_timepoint("start")) - self.add_timepoint(finish_node_id, task, "finish") - self.add_timepoint_constraints(finish_node_id, task, "finish") + self.add_timepoint(pickup_node_id, task, "pickup", action_id=task.pickup_action_id) + self.add_timepoint_constraint(pickup_node_id, task.get_timepoint("pickup")) + + self.add_timepoint(delivery_node_id, task, "delivery", action_id=task.delivery_action_id) + self.add_timepoint_constraint(delivery_node_id, task.get_timepoint("delivery")) # Add constraints between new nodes - new_constraints_between = [navigation_node_id, start_node_id, finish_node_id] + new_constraints_between = [start_node_id, pickup_node_id, delivery_node_id] # Check if there is a node after the new delivery node - if self.has_node(finish_node_id+1): - new_constraints_between.append(finish_node_id+1) + if self.has_node(delivery_node_id+1): + new_constraints_between.append(delivery_node_id+1) # Check if there is a node before the new start node - if self.has_node(navigation_node_id-1): - new_constraints_between.insert(0, navigation_node_id-1) + if self.has_node(start_node_id-1): + new_constraints_between.insert(0, start_node_id-1) self.logger.debug("New constraints between nodes: %s", new_constraints_between) @@ -193,9 +220,9 @@ def add_task(self, task, position=1): def add_intertimepoints_constraints(self, constraints, task): """ Adds constraints between the timepoints of a task Constraints between: - - navigation start and start - - start and finish - - finish and next task (if any) + - start and pickup + - pickup and delivery + - delivery and start next task (if any) Args: constraints (list) : list of tuples that defines the pair of nodes between which a new constraint should be added Example: @@ -206,81 +233,98 @@ def add_intertimepoints_constraints(self, constraints, task): """ for (i, j) in constraints: self.logger.debug("Adding constraint: %s ", (i, j)) - if self.node[i]['data']['node_type'] == "navigation": - duration = self.get_navigation_duration(i, j) - self.add_constraint(i, j, duration) + if self.nodes[i]['data'].node_type == "start": + travel_time = self.get_travel_time(task) + self.add_constraint(i, j, travel_time, travel_time) - elif self.node[i]['data']['node_type'] == "start": - duration = self.get_task_duration(task) - self.add_constraint(i, j, duration) + elif self.nodes[i]['data'].node_type == "pickup": + work_time = self.get_work_time(task) + self.add_constraint(i, j, work_time, work_time) - elif self.node[i]['data']['node_type'] == "finish": + elif self.nodes[i]['data'].node_type == "delivery": # wait time between finish of one task and start of the next one. Fixed to [0, inf] self.add_constraint(i, j) - def get_navigation_duration(self, source, destination): - """ Reads from the database the estimated duration for navigating from source to destination and takes the mean + @staticmethod + def get_travel_time(task): + """ Returns the mean of the travel time (time for going from current pose to pickup pose) """ - # TODO: Read estimated duration from dataset - duration = 1.0 - return duration + travel_time = task.get_edge("travel_time") + return travel_time.mean - def get_task_duration(self, task): - """ Reads from the database the estimated duration of the task - In the case of transportation tasks, the estimated duration is the navigation time from the pickup to the delivery location + @staticmethod + def get_work_time(task): + """ Returns the mean of the work time (time to transport an object from the pickup to the delivery location) """ - # TODO: Read estimated duration from dataset - duration = 1.0 - return duration + work_time = task.get_edge("work_time") + return work_time.mean + + @staticmethod + def create_timepoint_constraints(r_earliest_pickup, r_latest_pickup, travel_time, work_time): + start_constraint = Timepoint(name="start", + r_earliest_time=r_earliest_pickup - travel_time.mean, + r_latest_time=r_latest_pickup - travel_time.mean) + pickup_constraint = Timepoint(name="pickup", + r_earliest_time=r_earliest_pickup, + r_latest_time=r_latest_pickup) + delivery_constraint = Timepoint(name="delivery", + r_earliest_time=r_earliest_pickup + work_time.mean, + r_latest_time=r_latest_pickup + work_time.mean) + return [start_constraint, pickup_constraint, delivery_constraint] - def get_navigation_start_time(self, task): - """ Returns the earliest_start_time and latest start navigation time + def show_n_nodes_edges(self): + """ Prints the number of nodes and edges in the stn """ - navigation_duration = self.get_navigation_duration(task.start_pose_name, task.finish_pose_name) + self.logger.info("Nodes: %s ", self.number_of_nodes()) + self.logger.info("Edges: %s ", self.number_of_edges()) - earliest_navigation_start_time = task.r_earliest_start_time - navigation_duration - latest_navigation_start_time = task.r_latest_start_time - navigation_duration + def update_task(self, task): + position = self.get_task_position(task.task_id) + start_node_id = 2 * position + (position-2) + pickup_node_id = start_node_id + 1 + delivery_node_id = pickup_node_id + 1 - return earliest_navigation_start_time, latest_navigation_start_time + # Adding an existing timepoint constraint updates the constraint + self.add_timepoint_constraint(start_node_id, task.get_timepoint("start")) + self.add_timepoint_constraint(pickup_node_id, task.get_timepoint("pickup")) + self.add_timepoint_constraint(delivery_node_id, task.get_timepoint("delivery")) - def get_finish_time(self, task): - """ Returns the earliest and latest finish time - """ - task_duration = self.get_task_duration(task) + # Add constraints between new nodes + new_constraints_between = [start_node_id, pickup_node_id, delivery_node_id] - earliest_finish_time = task.r_earliest_start_time + task_duration - latest_finish_time = task.r_latest_start_time + task_duration + # Check if there is a node after the new delivery node + if self.has_node(delivery_node_id+1): + new_constraints_between.append(delivery_node_id+1) - return earliest_finish_time, latest_finish_time + # Check if there is a node before the new start node + if self.has_node(start_node_id-1): + new_constraints_between.insert(0, start_node_id-1) - def show_n_nodes_edges(self): - """ Prints the number of nodes and edges in the stn - """ - self.logger.info("Nodes: %s ", self.number_of_nodes()) - self.logger.info("Edges: %s ", self.number_of_edges()) + constraints = [((i), (i + 1)) for i in new_constraints_between[:-1]] + self.add_intertimepoints_constraints(constraints, task) def remove_task(self, position=1): """ Removes the task from the given position""" self.logger.info("Removing task at position: %s", position) - navigation_node_id = 2 * position + (position-2) - start_node_id = navigation_node_id + 1 - finish_node_id = start_node_id + 1 + start_node_id = 2 * position + (position-2) + pickup_node_id = start_node_id + 1 + delivery_node_id = pickup_node_id + 1 new_constraints_between = list() - if self.has_node(navigation_node_id-1) and self.has_node(finish_node_id+1): - new_constraints_between = [navigation_node_id-1, navigation_node_id] + if self.has_node(start_node_id-1) and self.has_node(delivery_node_id+1): + new_constraints_between = [start_node_id-1, start_node_id] # Remove node and all adjacent edges - self.remove_node(navigation_node_id) self.remove_node(start_node_id) - self.remove_node(finish_node_id) + self.remove_node(pickup_node_id) + self.remove_node(delivery_node_id) # Displace by -3 all nodes and constraints after position mapping = {} for node_id, data in self.nodes(data=True): - if node_id >= navigation_node_id: + if node_id >= start_node_id: mapping[node_id] = node_id - 3 self.logger.debug("mapping: %s", mapping) nx.relabel_nodes(self, mapping, copy=False) @@ -290,10 +334,22 @@ def remove_task(self, position=1): self.logger.debug("Constraints: %s", constraints) for (i, j) in constraints: - if self.node[i]['data']['node_type'] == "finish": + if self.nodes[i]['data'].node_type == "delivery": # wait time between finish of one task and start of the next one self.add_constraint(i, j) + def remove_node_ids(self, node_ids): + # Assumes that the node_ids are in consecutive order from node_id 1 onwards + for node_id in node_ids: + self.remove_node(node_id) + + # Displace all remaining nodes by 3 + mapping = {} + for node_id, data in self.nodes(data=True): + if node_id > 0: + mapping[node_id] = node_id - 3 + nx.relabel_nodes(self, mapping, copy=False) + def get_tasks(self): """ Gets the tasks (in order) @@ -302,17 +358,17 @@ def get_tasks(self): """ tasks = list() for i in self.nodes(): - timepoint = Node.from_dict(self.node[i]['data']) - if timepoint.node_type == "navigation": - tasks.append(timepoint.task_id) - + if self.nodes[i]['data'].task_id not in tasks and self.nodes[i]['data'].node_type != 'zero_timepoint': + tasks.append(self.nodes[i]['data'].task_id) return tasks def is_consistent(self, shortest_path_array): """The STN is not consistent if it has negative cycles""" consistent = True for node, nodes in shortest_path_array.items(): - if nodes[node] != 0: + # Check if the tolerance is too large. Maybe it is better to use + # only integers and change the resolution to seconds + if not math.isclose(nodes[node], 0.0, abs_tol=1e-01): consistent = False return consistent @@ -324,7 +380,7 @@ def update_edges(self, shortest_path_array, create=False): for n in nodes: self.update_edge_weight(column, n, shortest_path_array[column][n]) - def update_edge_weight(self, i, j, weight, create=False): + def update_edge_weight(self, i, j, weight, force=False): """ Updates the weight of the edge between node starting_node and node ending_node Updates the weight if the new weight is less than the previous weight @@ -344,19 +400,27 @@ def update_edge_weight(self, i, j, weight, create=False): if weight < self[i][j]['weight']: self[i][j]['weight'] = weight - def assign_timepoint(self, time, position=1): + if force: + self[i][j]['weight'] = weight + + def assign_timepoint(self, allotted_time, node_id, force=False): """ - Assigns the given time to the earliest and latest time of the - timepoint at the given position + Assigns the allotted time to the earliest and latest time of the timepoint + in node_id Args: - time: float representing seconds - position: int representing the location of the timepoint in the stn - - Returns: + allotted_time (float): seconds after zero timepoint + node_id (inf): idx of the timepoint in the stn """ - self.update_edge_weight(0, position, time) - self.update_edge_weight(position, 0, -time) + self.update_edge_weight(0, node_id, allotted_time, force) + self.update_edge_weight(node_id, 0, -allotted_time, force) + + def assign_earliest_time(self, time_, task_id, node_type, force=False): + for i in self.nodes(): + node_data = self.nodes[i]['data'] + if node_data.task_id == task_id and node_data.node_type == node_type: + self.update_edge_weight(i, 0, -time_, force) + break def get_edge_weight(self, i, j): """ Returns the weight of the edge between node starting_node and node ending_node @@ -371,19 +435,22 @@ def get_edge_weight(self, i, j): else: return float('inf') - def get_completion_time(self): - nodes = list(self.nodes()) - node_first_task = nodes[1] - node_last_task = nodes[-1] - - start_time_lower_bound = -self[node_first_task][0]['weight'] - - finish_time_lower_bound = -self[node_last_task][0]['weight'] - - self.logger.debug("Start time: %s", start_time_lower_bound) - self.logger.debug("Finish time: %s", finish_time_lower_bound) + def compute_temporal_metric(self, temporal_criterion): + if temporal_criterion == 'completion_time': + temporal_metric = self.get_completion_time() + elif temporal_criterion == 'makespan': + temporal_metric = self.get_makespan() + elif temporal_criterion == 'idle_time': + temporal_metric = self.get_idle_time() + else: + raise ValueError(temporal_criterion) + return temporal_metric - completion_time = finish_time_lower_bound - start_time_lower_bound + def get_completion_time(self): + completion_time = 0 + task_ids = self.get_tasks() + for i, task_id in enumerate(task_ids): + completion_time += self.get_time(task_id, "delivery", lower_bound=False) return completion_time @@ -400,58 +467,75 @@ def get_idle_time(self): for i, task_id in enumerate(task_ids): if i > 0: - r_earliest_finish_time_previous_task = self.get_time(task_ids[i-1], "finish") + r_earliest_delivery_time_previous_task = self.get_time(task_ids[i-1], "delivery") r_earliest_start_time = self.get_time(task_ids[i], "start") - idle_time += round(r_earliest_start_time - r_earliest_finish_time_previous_task) + idle_time += round(r_earliest_start_time - r_earliest_delivery_time_previous_task) return idle_time - def add_timepoint_constraints(self, node_id, task, node_type): + def add_timepoint_constraint(self, node_id, timepoint_constraint): """ Adds the earliest and latest times to execute a timepoint (node) - Navigation timepoint [earliest_navigation_start_time, latest_navigation_start_time] Start timepoint [earliest_start_time, latest_start_time] - Finish timepoint [earliest_finish_time, lastest_finish_time] + Pickup timepoint [earliest_pickup_time, latest_pickup_time] + Delivery timepoint [earliest_delivery_time, lastest_delivery_time] """ + self.add_constraint(0, node_id, timepoint_constraint.r_earliest_time, timepoint_constraint.r_latest_time) - if task.hard_constraints: - self.timepoint_hard_constraints(node_id, task, node_type) - else: - self.timepoint_soft_constraints(node_id, task, node_type) + @staticmethod + def get_prev_timepoint(timepoint_name, next_timepoint, edge_in_between): + r_earliest_time = next_timepoint.r_earliest_time - edge_in_between.mean + r_latest_time = next_timepoint.r_latest_time - edge_in_between.mean + return Timepoint(timepoint_name, r_earliest_time, r_latest_time) - def timepoint_hard_constraints(self, node_id, task, node_type): - if node_type == "navigation": - earliest_navigation_start_time, latest_navigation_start_time = self.get_navigation_start_time(task) + @staticmethod + def get_next_timepoint(timepoint_name, prev_timepoint, edge_in_between): + r_earliest_time = prev_timepoint.r_earliest_time + edge_in_between.mean + r_latest_time = prev_timepoint.r_latest_time + edge_in_between.mean + return Timepoint(timepoint_name, r_earliest_time, r_latest_time) - self.add_constraint(0, node_id, earliest_navigation_start_time, latest_navigation_start_time) + def get_time(self, task_id, node_type='start', lower_bound=True): + _time = None + for i, data in self.nodes.data(): - if node_type == "start": - self.add_constraint(0, node_id, task.r_earliest_start_time, task.r_latest_start_time) + if task_id == data['data'].task_id and data['data'].node_type == node_type: + if lower_bound: + _time = -self[i][0]['weight'] + else: # upper bound + _time = self[0][i]['weight'] - elif node_type == "finish": - earliest_finish_time, latest_finish_time = self.get_finish_time(task) + return _time - self.add_constraint(0, node_id, earliest_finish_time, latest_finish_time) + def get_node_earliest_time(self, node_id): + return -self[node_id][0]['weight'] - def timepoint_soft_constraints(self, node_id, task, node_type): - if node_type == "navigation": - self.add_constraint(0, node_id, task.r_earliest_navigation_start) + def get_node_latest_time(self, node_id): + return self[0][node_id]['weight'] - if node_type == "start": - self.add_constraint(0, node_id) + def get_nodes_by_action(self, action_id): + nodes = list() + for node_id, data in self.nodes.data(): + if data['data'].action_id == action_id: + node = (node_id, self.nodes[node_id]['data']) + nodes.append(node) + return nodes - elif node_type == "finish": + def get_nodes_by_task(self, task_id): + nodes = list() + for node_id, data in self.nodes.data(): + if data['data'].task_id == task_id: + node = (node_id, self.nodes[node_id]['data']) + nodes.append(node) + return nodes - self.add_constraint(0, node_id, 0, self.max_makespan) + def get_node_by_type(self, task_id, node_type): + for node_id, data in self.nodes.data(): + if data['data'].task_id == task_id and data['data'].node_type == node_type: + return node_id, self.nodes[node_id]['data'] - def get_time(self, task_id, node_type='navigation', lower_bound=True): - _time = None - for i, data in self.nodes.data(): - if task_id == data['data']['task_id'] and data['data']['node_type'] == node_type: - if lower_bound: - _time = -self[i][0]['weight'] - else: # upper bound - _time = self[0][i]['weight'] + def set_action_id(self, node_id, action_id): + self.nodes[node_id]['data'].action_id = action_id - return _time + def get_node(self, node_id): + return self.nodes[node_id]['data'] def get_task_id(self, position): """ Returns the id of the task in the given position @@ -462,16 +546,27 @@ def get_task_id(self, position): Returns: (string) task id """ - navigation_node = 2 * position + (position-2) - - if self.has_node(navigation_node): - task_id = self.node[navigation_node]['data']['task_id'] + start_node_id = 2 * position + (position-2) + pickup_node_id = start_node_id + 1 + delivery_node_id = pickup_node_id + 1 + + if self.has_node(start_node_id): + task_id = self.nodes[start_node_id]['data'].task_id + elif self.has_node(pickup_node_id): + task_id = self.nodes[pickup_node_id]['data'].task_id + elif self.has_node(delivery_node_id): + task_id = self.nodes[delivery_node_id]['data'].task_id else: self.logger.error("There is no task in position %s", position) return return task_id + def get_task_position(self, task_id): + for i, data in self.nodes.data(): + if task_id == data['data'].task_id and data['data'].node_type == 'start': + return math.ceil(i/3) + def get_earliest_task_id(self): """ Returns the id of the earliest task in the stn @@ -480,12 +575,28 @@ def get_earliest_task_id(self): # The first task in the graph is the task with the earliest start time # The first task is in node 1, node 0 is reserved for the zero timepoint - task_id = self.get_task_id(1) - if task_id: + if self.has_node(1): + task_id = self.nodes[1]['data'].task_id return task_id self.logger.debug("STN has no tasks yet") + def get_task_nodes(self, task_id): + """ Gets the nodes in the stn associated with the given task_id + + Args: + task_id: (string) id of the task + + Returns: list of node ids + + """ + nodes = list() + for i in self.nodes(): + if task_id == self.nodes[i]['data'].task_id: + nodes.append(self.nodes[i]['data']) + + return nodes + def get_task_node_ids(self, task_id): """ Gets the node_ids in the stn associated with the given task_id @@ -497,11 +608,25 @@ def get_task_node_ids(self, task_id): """ node_ids = list() for i in self.nodes(): - if task_id == self.node[i]['data']['task_id']: + if task_id == self.nodes[i]['data'].task_id: node_ids.append(i) return node_ids + def get_task_graph(self, task_id): + """ Returns a graph with the nodes of the task_id + + Args: + task_id: ID of the task + + Returns: nx graph with nodes of task_id + + """ + node_ids = self.get_task_node_ids(task_id) + node_ids.insert(0, 0) + task_graph = self.subgraph(node_ids) + return task_graph + def get_subgraph(self, n_tasks): """ Returns a subgraph of the stn that includes the nodes of the first n_tasks and the zero timepoint @@ -523,6 +648,51 @@ def get_subgraph(self, n_tasks): sub_graph = self.subgraph(node_ids) return sub_graph + def execute_timepoint(self, node_id): + self.nodes[node_id]['data'].is_executed = True + + def execute_edge(self, node_1, node_2): + nx.set_edge_attributes(self, {(node_1, node_2): {'is_executed': True}, + (node_2, node_1): {'is_executed': True}}) + + def execute_incoming_edge(self, task_id, node_type): + finish_node_idx = self.get_edge_node_idx(task_id, node_type) + if node_type == "start": + return + elif node_type == "pickup": + start_node_idx = self.get_edge_node_idx(task_id, "start") + elif node_type == "delivery": + start_node_idx = self.get_edge_node_idx(task_id, "pickup") + self.execute_edge(start_node_idx, finish_node_idx) + + def remove_old_timepoints(self): + nodes_to_remove = list() + for i in self.nodes(): + node_data = self.nodes[i]['data'] + if not node_data.is_executed: + continue + if node_data.is_executed and (self.has_edge(i, i+1) and self[i][i+1]['is_executed']): + nodes_to_remove.append(i) + + for node in nodes_to_remove: + self.remove_node(node) + + def get_edge_node_idx(self, task_id, node_type): + for i in self.nodes(): + node_data = self.nodes[i]['data'] + if node_data.task_id == task_id and node_data.node_type == node_type: + return i + + def get_edge_nodes_idx(self, task_id, node_type_1, node_type_2): + for i in self.nodes(): + node_data = self.nodes[i]['data'] + if node_data.task_id == task_id and node_data.node_type == node_type_1: + start_node_idx = i + elif node_data.task_id == task_id and node_data.node_type == node_type_2: + finish_node_idx = i + + return start_node_idx, finish_node_idx + def to_json(self): stn_dict = self.to_dict() MyEncoder().encode(stn_dict) @@ -530,7 +700,10 @@ def to_json(self): return stn_json def to_dict(self): - stn_dict = json_graph.node_link_data(self) + stn = copy.deepcopy(self) + for i, data in self.nodes.data(): + stn.nodes[i]['data'] = self.nodes[i]['data'].to_dict() + stn_dict = json_graph.node_link_data(stn) return stn_dict @classmethod @@ -538,7 +711,7 @@ def from_json(cls, stn_json): stn = cls() dict_json = json.loads(stn_json) graph = json_graph.node_link_graph(dict_json) - stn.add_nodes_from(graph.nodes(data=True)) + stn.add_nodes_from([(i, {'data': Node.from_dict(graph.nodes[i]['data'])}) for i in graph.nodes()]) stn.add_edges_from(graph.edges(data=True)) return stn diff --git a/stn/stnu/stnu.py b/stn/stnu/stnu.py index 8047899..cacbac6 100644 --- a/stn/stnu/stnu.py +++ b/stn/stnu/stnu.py @@ -1,7 +1,7 @@ from stn.stn import STN -from stn.stn import Node from json import JSONEncoder import logging +from stn.task import Timepoint class MyEncoder(JSONEncoder): @@ -23,17 +23,23 @@ def __str__(self): if self.has_edge(j, i) and i < j: # Constraints with the zero timepoint if i == 0: - timepoint = Node.from_dict(self.node[j]['data']) + timepoint = self.nodes[j]['data'] lower_bound = -self[j][i]['weight'] upper_bound = self[i][j]['weight'] to_print += "Timepoint {}: [{}, {}]".format(timepoint, lower_bound, upper_bound) + if timepoint.is_executed: + to_print += " Ex" # Constraints between the other timepoints else: if self[j][i]['is_contingent'] is True: to_print += "Constraint {} => {}: [{}, {}] (contingent)".format(i, j, -self[j][i]['weight'], self[i][j]['weight']) + if self[i][j]['is_executed']: + to_print += " Ex" else: to_print += "Constraint {} => {}: [{}, {}]".format(i, j, -self[j][i]['weight'], self[i][j]['weight']) + if self[i][j]['is_executed']: + to_print += " Ex" to_print += "\n" @@ -68,25 +74,8 @@ def add_constraint(self, i, j, wji=0.0, wij=float('inf'), is_contingent=False): super().add_constraint(i, j, wji, wij) self.add_edge(i, j, is_contingent=is_contingent) - self.add_edge(j, i, is_contingent=is_contingent) - def timepoint_hard_constraints(self, node_id, task, node_type): - """ Adds the earliest and latest times to execute a timepoint (node) - Navigation timepoint [0, inf] - Start timepoint [earliest_start_time, latest_start_time] - Finish timepoint [0, inf] - """ - - if node_type == "navigation": - self.add_constraint(0, node_id, task.r_earliest_navigation_start_time) - - if node_type == "start": - self.add_constraint(0, node_id, task.r_earliest_start_time, task.r_latest_start_time) - - elif node_type == "finish": - self.add_constraint(0, node_id) - def get_contingent_constraints(self): """ Returns a dictionary with the contingent constraints in the STNU {(starting_node, ending_node): self[i][j] } @@ -102,12 +91,11 @@ def get_contingent_constraints(self): def get_contingent_timepoints(self): """ Returns a list with the contingent (uncontrollable) timepoints in the STNU """ - timepoints = list(self.nodes) contingent_timepoints = list() for (i, j, data) in self.edges.data(): if self[i][j]['is_contingent'] is True and i < j: - contingent_timepoints.append(timepoints[j]) + contingent_timepoints.append(j) return contingent_timepoints @@ -132,56 +120,63 @@ def add_intertimepoints_constraints(self, constraints, task): """ for (i, j) in constraints: self.logger.debug("Adding constraint: %s ", (i, j)) - if self.node[i]['data']['node_type'] == "navigation": - lower_bound, upper_bound = self.get_navigation_bounded_duration(i, j) - self.add_constraint(i, j, lower_bound, upper_bound, is_contingent=True) + if self.nodes[i]['data'].node_type == "start": + lower_bound, upper_bound = self.get_travel_time_bounded_duration(task) + if lower_bound == upper_bound: + self.add_constraint(i, j, 0, 0) + else: + self.add_constraint(i, j, lower_bound, upper_bound, is_contingent=True) - elif self.node[i]['data']['node_type'] == "start": - lower_bound, upper_bound = self.get_task_bounded_duration(task) + elif self.nodes[i]['data'].node_type == "pickup": + lower_bound, upper_bound = self.get_work_time_bounded_duration(task) self.add_constraint(i, j, lower_bound, upper_bound, is_contingent=True) - elif self.node[i]['data']['node_type'] == "finish": + elif self.nodes[i]['data'].node_type == "delivery": # wait time between finish of one task and start of the next one. Fixed to [0, inf] self.add_constraint(i, j, 0) - def get_navigation_bounded_duration(self, source, destination): - """ Reads from the database the probability distribution for navigating from source to destination and converts it to a bounded interval + @staticmethod + def get_travel_time_bounded_duration(task): + """ Returns the estimated travel time as a bounded interval [mu - 2*sigma, mu + 2*sigma] as in: Shyan Akmal, Savana Ammons, Hemeng Li, and James Boerkoel Jr. Quantifying Degrees of Controllability in Temporal Networks with Uncertainty. In Proceedings of the 29th International Conference on Automated Planning and Scheduling, ICAPS 2019, 07 2019. """ - # TODO: Read estimated distribution from database - distribution = "N_1_1" - name_split = distribution.split("_") - # mean - mu = float(name_split[1]) - # standard deviation - sigma = float(name_split[2]) - - lower_bound = mu - 2*sigma - upper_bound = mu + 2*sigma + travel_time = task.get_edge("travel_time") + lower_bound = travel_time.mean - 2*travel_time.standard_dev + upper_bound = travel_time.mean + 2*travel_time.standard_dev return lower_bound, upper_bound - def get_task_bounded_duration(self, task): - """ Reads from the database the estimated distribution of the task - In the case of transportation tasks, the estimated distribution is the navigation time from the pickup to the delivery location - Converts the estimated distribution to a bounded interval + @staticmethod + def get_work_time_bounded_duration(task): + """ Returns the estimated work time as a bounded interval [mu - 2*sigma, mu + 2*sigma] as in: Shyan Akmal, Savana Ammons, Hemeng Li, and James Boerkoel Jr. Quantifying Degrees of Controllability in Temporal Networks with Uncertainty. In Proceedings of the 29th International Conference on Automated Planning and Scheduling, ICAPS 2019, 07 2019. """ - # TODO: Read estimated distribution from database - distribution = "N_4_1" - name_split = distribution.split("_") - # mean - mu = float(name_split[1]) - # standard deviation - sigma = float(name_split[2]) - - lower_bound = mu - 2*sigma - upper_bound = mu + 2*sigma + work_time = task.get_edge("work_time") + lower_bound = work_time.mean - 2*work_time.standard_dev + upper_bound = work_time.mean + 2*work_time.standard_dev return lower_bound, upper_bound + + @staticmethod + def get_prev_timepoint(timepoint_name, next_timepoint, edge_in_between): + r_earliest_time = next_timepoint.r_earliest_time - \ + (edge_in_between.mean + 2*edge_in_between.standard_dev) + r_latest_time = next_timepoint.r_latest_time - \ + (edge_in_between.mean - 2*edge_in_between.standard_dev) + + return Timepoint(timepoint_name, r_earliest_time, r_latest_time) + + @staticmethod + def get_next_timepoint(timepoint_name, prev_timepoint, edge_in_between): + r_earliest_time = prev_timepoint.r_earliest_time + \ + (edge_in_between.mean - 2*edge_in_between.standard_dev) + r_latest_time = prev_timepoint.r_latest_time + \ + (edge_in_between.mean + 2*edge_in_between.standard_dev) + + return Timepoint(timepoint_name, r_earliest_time, r_latest_time) diff --git a/stn/stp.py b/stn/stp.py index 3d142d4..648f82e 100644 --- a/stn/stp.py +++ b/stn/stp.py @@ -1,5 +1,7 @@ +import networkx as nx + from stn.config.config import stn_factory, stp_solver_factory -from stn.methods.fpc import get_minimal_network +from stn.exceptions.stp import NoSTPSolution """ Solves a Simple Temporal Problem (STP) @@ -48,26 +50,18 @@ def get_stn(self, **kwargs): def solve(self, stn): """ Computes the dispatchable graph and risk metric of the given stn """ - result_stp = self.solver.compute_dispatchable_graph(stn) - return result_stp + dispatchable_graph = self.solver.compute_dispatchable_graph(stn) - @staticmethod - def compute_temporal_metric(dispatchable_graph, temporal_criterion): - if temporal_criterion == 'completion_time': - temporal_metric = dispatchable_graph.get_completion_time() - elif temporal_criterion == 'makespan': - temporal_metric = dispatchable_graph.get_makespan() - else: - raise ValueError(temporal_criterion) + if dispatchable_graph is None: + raise NoSTPSolution() - return temporal_metric + return dispatchable_graph @staticmethod def is_consistent(stn): - minimal_network = get_minimal_network(stn) - if minimal_network: + shortest_path_array = nx.floyd_warshall(stn) + if stn.is_consistent(shortest_path_array): return True - else: - return False + return False diff --git a/stn/task.py b/stn/task.py index dc134ed..da37fa1 100644 --- a/stn/task.py +++ b/stn/task.py @@ -1,28 +1,131 @@ -class STNTask(object): - def __init__(self, id, - r_earliest_navigation_start_time, - r_earliest_start_time, - r_latest_start_time, - start_pose_name, - finish_pose_name, - **kwargs): +from stn.utils.as_dict import AsDictMixin + + +class Edge(AsDictMixin): + def __init__(self, name, mean, variance, **kwargs): + self.name = name + self.mean = round(mean, 3) + self.variance = round(variance, 3) + self.standard_dev = round(variance ** 0.5, 3) + + def __str__(self): + to_print = "" + to_print += "{}: N({}, {})".format(self.name, self.mean, self.standard_dev) + return to_print + + def __sub__(self, other): + # Difference of two independent random variables + mean = self.mean - other.mean + variance = self.variance + other.variance + return mean, variance + + def __add__(self, other): + # Addition of two independent random variables + mean = self.mean + other.mean + variance = self.variance + other.variance + return mean, variance + + +class Timepoint(AsDictMixin): + """ + r_earliest_time (float): earliest time relative to a ztp + r_latest_time (float): latest time relative to a ztp + + """ + def __init__(self, name, r_earliest_time, r_latest_time, **kwargs): + self.name = name + self.r_earliest_time = round(r_earliest_time, 3) + self.r_latest_time = round(r_latest_time, 3) + + def __str__(self): + to_print = "" + to_print += "{}: [{}, {}]".format(self.name, self.r_earliest_time, self.r_latest_time) + return to_print + + +class Task(AsDictMixin): + def __init__(self, task_id, timepoints, edges, pickup_action_id, delivery_action_id): """ Constructor for the Task object Args: - id (UUID): An instance of an UUID object - r_earliest_navigation_start_time (float): earliest navigation start time relative to the ztp - r_earliest_start_time (float): earliest start time relative to the ztp - r_latest_start_time (float): latest start time relative to the ztp - start_pose_name (str): Name of the location where the robot should execute the task - finish_pose_name (str): Name of the location where the robot must terminate task execution - hard_constraints (bool): False if the task can be - scheduled ASAP, True if the task is not flexible. Defaults to True + task_id (UUID): An instance of an UUID object + timepoints (list): list of timepoints (Timepoints) + Edges (list): list of edges (Edges) + pickup_action_id (UUID): Action id of the pickup action + delivery_action_id (UUID): Action id of te delivery action """ - self.id = id - self.r_earliest_navigation_start_time = round(r_earliest_navigation_start_time, 2) - self.r_earliest_start_time = round(r_earliest_start_time, 2) - self.r_latest_start_time = round(r_latest_start_time, 2) - self.start_pose_name = start_pose_name - self.finish_pose_name = finish_pose_name - self.hard_constraints = kwargs.get('hard_constraints', True) + self.task_id = task_id + self.timepoints = timepoints + self.edges = edges + self.pickup_action_id = pickup_action_id + self.delivery_action_id = delivery_action_id + + def __str__(self): + to_print = "" + to_print += "{} \n".format(self.task_id) + to_print += "Timepoints: \n" + for timepoint in self.timepoints: + to_print += str(timepoint) + "\t" + to_print += "\n Edges: \n" + for edge in self.edges: + to_print += str(edge) + "\t" + to_print += "\n Pickup action:" + str(self.pickup_action_id) + to_print += "\n Delivery action:" + str(self.delivery_action_id) + return to_print + + def get_timepoint(self, timepoint_name): + for timepoint in self.timepoints: + if timepoint.name == timepoint_name: + return timepoint + + def get_edge(self, edge_name): + for edge in self.edges: + if edge.name == edge_name: + return edge + + def update_timepoint(self, timepoint_name, r_earliest_time, r_latest_time=float('inf')): + in_list = False + for timepoint in self.timepoints: + if timepoint.name == timepoint_name: + in_list = True + timepoint.r_earliest_time = r_earliest_time + timepoint.r_latest_time = r_latest_time + if not in_list: + self.timepoints.append(Timepoint(timepoint_name, r_earliest_time, r_latest_time)) + + def update_edge(self, edge_name, mean, variance): + in_list = False + for edge in self.edges: + if edge.name == edge_name: + in_list = True + edge.mean = round(mean, 3) + edge.variance = round(variance, 3) + edge.standard_dev = round(variance ** 0.5, 3) + if not in_list: + self.edges.append(Edge(name=edge_name, mean=mean, variance=variance)) + + def to_dict(self): + dict_repr = super().to_dict() + timepoints = list() + edges = list() + for t in self.timepoints: + timepoints.append(t.to_dict()) + for e in self.edges: + edges.append(e.to_dict()) + dict_repr.update(timepoints=timepoints) + dict_repr.update(edges=edges) + return dict_repr + + @classmethod + def to_attrs(cls, dict_repr): + attrs = super().to_attrs(dict_repr) + timepoints = list() + edges = list() + for t in attrs.get("timepoints"): + timepoints.append(Timepoint.from_dict(t)) + for e in attrs.get("edges"): + edges.append(Edge.from_dict(e)) + attrs.update(timepoints=timepoints) + attrs.update(edges=edges) + return attrs diff --git a/stn/utils/as_dict.py b/stn/utils/as_dict.py new file mode 100644 index 0000000..7a24687 --- /dev/null +++ b/stn/utils/as_dict.py @@ -0,0 +1,50 @@ +""" Adapted from: +https://realpython.com/inheritance-composition-python/#mixing-features-with-mixin-classes +""" +import uuid +from stn.utils.uuid import from_str + + +class AsDictMixin: + + def to_dict(self): + return { + prop: self._represent(value) + for prop, value in self.__dict__.items() + if not self.is_internal(prop) + } + + @classmethod + def _represent(cls, value): + if isinstance(value, object): + if hasattr(value, 'to_dict'): + return value.to_dict() + elif isinstance(value, uuid.UUID): + return str(value) + else: + return value + else: + return value + + @staticmethod + def is_internal(prop): + return prop.startswith('_') + + @classmethod + def from_dict(cls, dict_repr): + attrs = cls.to_attrs(dict_repr) + return cls(**attrs) + + @classmethod + def to_attrs(cls, dict_repr): + attrs = dict() + for key, value in dict_repr.items(): + attrs[key] = cls._get_value(key, value) + return attrs + + @classmethod + def _get_value(cls, key, value): + if key in ['task_id', 'pickup_action_id', 'delivery_action_id']: + return from_str(value) + else: + return value diff --git a/stn/utils/config_logger.py b/stn/utils/config_logger.py deleted file mode 100644 index e865b7b..0000000 --- a/stn/utils/config_logger.py +++ /dev/null @@ -1,9 +0,0 @@ -import logging.config -import yaml - - -def config_logger(logging_file): - - with open(logging_file) as f: - log_config = yaml.safe_load(f) - logging.config.dictConfig(log_config) \ No newline at end of file diff --git a/stn/utils/utils.py b/stn/utils/utils.py new file mode 100644 index 0000000..94fab8b --- /dev/null +++ b/stn/utils/utils.py @@ -0,0 +1,36 @@ +import logging.config +import yaml +from stn.task import Task, Edge +from stn.utils.uuid import generate_uuid + + +def config_logger(logging_file): + + with open(logging_file) as f: + log_config = yaml.safe_load(f) + logging.config.dictConfig(log_config) + + +def load_yaml(file): + """ Reads a yaml file and returns a dictionary with its contents + + :param file: file to load + :return: data as dict() + """ + with open(file, 'r') as file: + data = yaml.safe_load(file) + return data + + +def create_task(stn, task_dict): + task_id = task_dict.get("task_id") + r_earliest_pickup = task_dict.get("earliest_pickup") + r_latest_pickup = task_dict.get("latest_pickup") + travel_time = Edge(**task_dict.get("travel_time")) + work_time = Edge(**task_dict.get("work_time")) + timepoint_constraints = stn.create_timepoint_constraints(r_earliest_pickup, r_latest_pickup, travel_time, work_time) + inter_timepoint_constraints = [travel_time, work_time] + pickup_action_id = generate_uuid() + delivery_action_id = generate_uuid() + + return Task(task_id, timepoint_constraints, inter_timepoint_constraints, pickup_action_id, delivery_action_id) diff --git a/stn/utils/uuid.py b/stn/utils/uuid.py index 16ed069..4d50f55 100644 --- a/stn/utils/uuid.py +++ b/stn/utils/uuid.py @@ -6,3 +6,10 @@ def generate_uuid(): Returns a string containing a random uuid """ return uuid.uuid4() + + +def from_str(uuid_str): + """ + Converts a uuid string to an uuid instance + """ + return uuid.UUID(uuid_str) diff --git a/test/data/pstn_two_tasks.json b/test/data/pstn_two_tasks.json index ac1e50c..06a5f14 100644 --- a/test/data/pstn_two_tasks.json +++ b/test/data/pstn_two_tasks.json @@ -5,154 +5,176 @@ "target":1, "weight":Infinity, "source":0, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":2, "weight":47.0, "source":0, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":3, "weight":Infinity, "source":0, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":4, "weight":Infinity, "source":0, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":5, "weight":102.0, "source":0, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":6, "weight":Infinity, "source":0, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":0, "weight":-0.0, "source":1, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"N_6_1", "target":2, "weight":Infinity, "source":1, - "is_contingent":true + "is_contingent":true, + "is_executed": false }, { "distribution":"", "target":0, "weight":-41.0, "source":2, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"N_6_1", "target":1, "weight":-0.0, "source":2, - "is_contingent":true + "is_contingent":true, + "is_executed": false }, { "distribution":"N_4_1", "target":3, "weight":Infinity, "source":2, - "is_contingent":true + "is_contingent":true, + "is_executed": false }, { "distribution":"", "target":0, "weight":-0.0, "source":3, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"N_4_1", "target":2, "weight":-0.0, "source":3, - "is_contingent":true + "is_contingent":true, + "is_executed": false }, { "distribution":"", "target":4, "weight":Infinity, "source":3, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":0, "weight":-0.0, "source":4, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"", "target":3, "weight":-0.0, "source":4, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"N_6_1", "target":5, "weight":Infinity, "source":4, - "is_contingent":true + "is_contingent":true, + "is_executed": false }, { "distribution":"", "target":0, "weight":-96.0, "source":5, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"N_6_1", "target":4, "weight":-0.0, "source":5, - "is_contingent":true + "is_contingent":true, + "is_executed": false }, { "distribution":"N_4_1", "target":6, "weight":Infinity, "source":5, - "is_contingent":true + "is_contingent":true, + "is_executed": false }, { "distribution":"", "target":0, "weight":-0.0, "source":6, - "is_contingent":false + "is_contingent":false, + "is_executed": false }, { "distribution":"N_4_1", "target":5, "weight":-0.0, "source":6, - "is_contingent":true + "is_contingent":true, + "is_executed": false } ], "graph":{ @@ -173,7 +195,7 @@ "data":{ "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", "pose":"", - "node_type":"navigation" + "node_type":"start" } }, { @@ -181,7 +203,7 @@ "data":{ "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", "pose":"AMK_TDU-TGR-1_X_9.7_Y_5.6", - "node_type":"start" + "node_type":"pickup" } }, { @@ -189,7 +211,7 @@ "data":{ "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", "pose":"AMK_TDU-TGR-1_X_5.82_Y_6.57", - "node_type":"finish" + "node_type":"delivery" } }, { @@ -197,7 +219,7 @@ "data":{ "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", "pose":"", - "node_type":"navigation" + "node_type":"start" } }, { @@ -205,7 +227,7 @@ "data":{ "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", "pose":"AMK_TDU-TGR-1_X_14.03_Y_9.55", - "node_type":"start" + "node_type":"pickup" } }, { @@ -213,7 +235,7 @@ "data":{ "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", "pose":"AMK_TDU-TGR-1_X_15.09_Y_5.69", - "node_type":"finish" + "node_type":"delivery" } } ], diff --git a/test/data/stn_two_tasks.json b/test/data/stn_two_tasks.json index bf9daa7..e0d0baa 100644 --- a/test/data/stn_two_tasks.json +++ b/test/data/stn_two_tasks.json @@ -3,112 +3,134 @@ { "source":0, "target":1, - "weight":41.0 + "weight":41.0, + "is_executed": false }, { "source":0, "target":2, - "weight":47.0 + "weight":47.0, + "is_executed": false }, { "source":0, "target":3, - "weight":51.0 + "weight":51.0, + "is_executed": false }, { "source":0, "target":4, - "weight":96.0 + "weight":96.0, + "is_executed": false }, { "source":0, "target":5, - "weight":102.0 + "weight":102.0, + "is_executed": false }, { "source":0, "target":6, - "weight":106.0 + "weight":106.0, + "is_executed": false }, { "source":1, "target":0, - "weight":-35.0 + "weight":-35.0, + "is_executed": false }, { "source":1, "target":2, - "weight":Infinity + "weight":Infinity, + "is_executed": false }, { "source":2, "target":0, - "weight":-41.0 + "weight":-41.0, + "is_executed": false }, { "source":2, "target":1, - "weight":-6.0 + "weight":-6.0, + "is_executed": false }, { "source":2, "target":3, - "weight":Infinity + "weight":Infinity, + "is_executed": false }, { "source":3, "target":0, - "weight":-45.0 + "weight":-45.0, + "is_executed": false }, { "source":3, "target":2, - "weight":-4.0 + "weight":-4.0, + "is_executed": false }, { "source":3, "target":4, - "weight":Infinity + "weight":Infinity, + "is_executed": false }, { "source":4, "target":0, - "weight":-90.0 + "weight":-90.0, + "is_executed": false }, { "source":4, "target":3, - "weight":-0.0 + "weight":-0.0, + "is_executed": false }, { "source":4, "target":5, - "weight":Infinity + "weight":Infinity, + "is_executed": false }, { "source":5, "target":0, - "weight":-96.0 + "weight":-96.0, + "is_executed": false }, { "source":5, "target":4, - "weight":-6.0 + "weight":-6.0, + "is_executed": false }, { "source":5, "target":6, - "weight":Infinity + "weight":Infinity, + "is_executed": false }, { "source":6, "target":0, - "weight":-100.0 + "weight":-100.0, + "is_executed": false }, { "source":6, "target":5, - "weight":-4.0 + "weight":-4.0, + "is_executed": false } ], "graph":{ @@ -128,7 +150,7 @@ { "data":{ "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", - "node_type":"navigation", + "node_type":"start", "pose":"" }, "id":1 @@ -136,7 +158,7 @@ { "data":{ "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", - "node_type":"start", + "node_type":"pickup", "pose":"AMK_TDU-TGR-1_X_9.7_Y_5.6" }, "id":2 @@ -144,7 +166,7 @@ { "data":{ "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", - "node_type":"finish", + "node_type":"delivery", "pose":"AMK_TDU-TGR-1_X_5.82_Y_6.57" }, "id":3 @@ -152,7 +174,7 @@ { "data":{ "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", - "node_type":"navigation", + "node_type":"start", "pose":"" }, "id":4 @@ -160,7 +182,7 @@ { "data":{ "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", - "node_type":"start", + "node_type":"pickup", "pose":"AMK_TDU-TGR-1_X_14.03_Y_9.55" }, "id":5 @@ -168,7 +190,7 @@ { "data":{ "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", - "node_type":"finish", + "node_type":"delivery", "pose":"AMK_TDU-TGR-1_X_15.09_Y_5.69" }, "id":6 diff --git a/test/data/stnu_two_tasks.json b/test/data/stnu_two_tasks.json index 9d659f5..2cc571b 100644 --- a/test/data/stnu_two_tasks.json +++ b/test/data/stnu_two_tasks.json @@ -10,7 +10,7 @@ }, { "data":{ - "node_type":"navigation", + "node_type":"start", "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", "pose":"" }, @@ -18,7 +18,7 @@ }, { "data":{ - "node_type":"start", + "node_type":"pickup", "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", "pose":"AMK_TDU-TGR-1_X_9.7_Y_5.6" }, @@ -26,7 +26,7 @@ }, { "data":{ - "node_type":"finish", + "node_type":"delivery", "task_id":"0d06fb90-a76d-48b4-b64f-857b7388ab70", "pose":"AMK_TDU-TGR-1_X_5.82_Y_6.57" }, @@ -34,7 +34,7 @@ }, { "data":{ - "node_type":"navigation", + "node_type":"start", "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", "pose":"" }, @@ -42,7 +42,7 @@ }, { "data":{ - "node_type":"start", + "node_type":"pickup", "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", "pose":"AMK_TDU-TGR-1_X_14.03_Y_9.55" }, @@ -50,7 +50,7 @@ }, { "data":{ - "node_type":"finish", + "node_type":"delivery", "task_id":"0616af00-ec3b-4ecd-ae62-c94a3703594c", "pose":"AMK_TDU-TGR-1_X_15.09_Y_5.69" }, @@ -63,133 +63,155 @@ "target":1, "is_contingent":false, "weight":Infinity, - "source":0 + "source":0, + "is_executed": false }, { "target":2, "is_contingent":false, "weight":47.0, - "source":0 + "source":0, + "is_executed": false }, { "target":3, "is_contingent":false, "weight":Infinity, - "source":0 + "source":0, + "is_executed": false }, { "target":4, "is_contingent":false, "weight":Infinity, - "source":0 + "source":0, + "is_executed": false }, { "target":5, "is_contingent":false, "weight":102.0, - "source":0 + "source":0, + "is_executed": false }, { "target":6, "is_contingent":false, "weight":Infinity, - "source":0 + "source":0, + "is_executed": false }, { "target":0, "is_contingent":false, "weight":-0.0, - "source":1 + "source":1, + "is_executed": false }, { "target":2, "is_contingent":true, "weight":8.0, - "source":1 + "source":1, + "is_executed": false }, { "target":0, "is_contingent":false, "weight":-41.0, - "source":2 + "source":2, + "is_executed": false }, { "target":1, "is_contingent":true, "weight":-4.0, - "source":2 + "source":2, + "is_executed": false }, { "target":3, "is_contingent":true, "weight":6.0, - "source":2 + "source":2, + "is_executed": false }, { "target":0, "is_contingent":false, "weight":-0.0, - "source":3 + "source":3, + "is_executed": false }, { "target":2, "is_contingent":true, "weight":-2.0, - "source":3 + "source":3, + "is_executed": false }, { "target":4, "is_contingent":false, "weight":Infinity, - "source":3 + "source":3, + "is_executed": false }, { "target":0, "is_contingent":false, "weight":-0.0, - "source":4 + "source":4, + "is_executed": false }, { "target":3, "is_contingent":false, "weight":0, - "source":4 + "source":4, + "is_executed": false }, { "target":5, "is_contingent":true, "weight":8.0, - "source":4 + "source":4, + "is_executed": false }, { "target":0, "is_contingent":false, "weight":-96.0, - "source":5 + "source":5, + "is_executed": false }, { "target":4, "is_contingent":true, "weight":-4.0, - "source":5 + "source":5, + "is_executed": false }, { "target":6, "is_contingent":true, "weight":6.0, - "source":5 + "source":5, + "is_executed": false }, { "target":0, "is_contingent":false, "weight":-0.0, - "source":6 + "source":6, + "is_executed": false }, { "target":5, "is_contingent":true, "weight":-2.0, - "source":6 + "source":6, + "is_executed": false } ], "graph":{ diff --git a/test/data/tasks.yaml b/test/data/tasks.yaml new file mode 100644 index 0000000..8ec79ca --- /dev/null +++ b/test/data/tasks.yaml @@ -0,0 +1,36 @@ +0616af00-ec3b-4ecd-ae62-c94a3703594c: + task_id: 0616af00-ec3b-4ecd-ae62-c94a3703594c + earliest_pickup: 10 + latest_pickup: 20 + travel_time: + name: "travel_time" + mean: 5 + variance: 0.2 + work_time: + name: "work_time" + mean: 10 + variance: 0.2 +207cc8da-2f0e-4538-802b-b8f3954df38d: + task_id: 207cc8da-2f0e-4538-802b-b8f3954df38d + earliest_pickup: 40 + latest_pickup: 50 + travel_time: + name: "travel_time" + mean: 5 + variance: 0.2 + work_time: + name: "work_time" + mean: 10 + variance: 0.2 +0d06fb90-a76d-48b4-b64f-857b7388ab70: + task_id: 0d06fb90-a76d-48b4-b64f-857b7388ab70 + earliest_pickup: 70 + latest_pickup: 80 + travel_time: + name: "travel_time" + mean: 5 + variance: 0.2 + work_time: + name: "work_time" + mean: 10 + variance: 0.2 diff --git a/test/scheduling_srea.py b/test/scheduling_srea.py index 789597a..cb3c73a 100644 --- a/test/scheduling_srea.py +++ b/test/scheduling_srea.py @@ -40,7 +40,7 @@ def get_schedule(dispatchable_graph, stn): n_tasks = 3 print("STN: ", stn) - alpha, dispatchable_graph = stp.solve(stn) + dispatchable_graph = stp.solve(stn) print("Guide: ", dispatchable_graph) schedule = get_schedule(dispatchable_graph, stn) diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..27a6c17 --- /dev/null +++ b/test/test.py @@ -0,0 +1,20 @@ +import json +from stn.stp import STP +STNU = "data/stnu_two_tasks.json" + + +if __name__ == '__main__': + with open(STNU) as json_file: + stnu_dict = json.load(json_file) + + # Convert the dict to a json string + stnu_json = json.dumps(stnu_dict) + + stp = STP('dsc_lp') + stn = stp.get_stn(stn_json=stnu_json) + + print(stn) + stn_dict = stn.to_dict() + + print(stn_dict) + print(type(stn_dict['nodes'][0]['data'])) diff --git a/test/test_dsc.py b/test/test_dsc.py index 600c63f..74bb9c4 100644 --- a/test/test_dsc.py +++ b/test/test_dsc.py @@ -29,16 +29,16 @@ def setUp(self): # Convert the dict to a json string stnu_json = json.dumps(stnu_dict) - self.stp = STP('dsc_lp') + self.stp = STP('dsc') self.stn = self.stp.get_stn(stn_json=stnu_json) def test_build_stn(self): self.logger.info("STNU: \n %s", self.stn) self.logger.info("Getting Schedule...") - risk_metric, schedule = self.stp.solve(self.stn) + schedule = self.stp.solve(self.stn) - self.logger.info("DSC: %s ", risk_metric) + self.logger.info("DSC: %s ", schedule.risk_metric) self.logger.info("schedule: %s ", schedule) completion_time = schedule.get_completion_time() @@ -47,11 +47,11 @@ def test_build_stn(self): self.logger.info("Completion time: %s ", completion_time) self.logger.info("Makespan: %s ", makespan) - self.assertEqual(completion_time, 61) + self.assertEqual(completion_time, 157) self.assertEqual(makespan, 98) expected_risk_metric = 0.0 - self.assertEqual(risk_metric, expected_risk_metric) + self.assertEqual(schedule.risk_metric, expected_risk_metric) constraints = schedule.get_constraints() diff --git a/test/test_fpc.py b/test/test_fpc.py index 64624c0..95b8dea 100644 --- a/test/test_fpc.py +++ b/test/test_fpc.py @@ -34,7 +34,7 @@ def setUp(self): def test_build_stn(self): self.logger.info("STN: \n %s", self.stn) - metric, minimal_network = self.stp.solve(self.stn) + minimal_network = self.stp.solve(self.stn) self.logger.info("Minimal STN: \n %s", minimal_network) @@ -43,7 +43,7 @@ def test_build_stn(self): self.logger.info("Completion time: %s ", completion_time) self.logger.info("Makespan: %s ", makespan) - self.assertEqual(completion_time, 65) + self.assertEqual(completion_time, 157) self.assertEqual(makespan, 100) constraints = minimal_network.get_constraints() diff --git a/test/test_srea.py b/test/test_srea.py index 1fd0783..8f8d8e0 100644 --- a/test/test_srea.py +++ b/test/test_srea.py @@ -38,79 +38,79 @@ def test_build_stn(self): self.logger.info("PSTN: \n %s", self.stn) self.logger.info("Getting GUIDE...") - alpha, guide_stn = self.stp.solve(self.stn) + dispatchable_graph = self.stp.solve(self.stn) self.logger.info("GUIDE") - self.logger.info(guide_stn) - self.logger.info("Alpha: %s ", alpha) + self.logger.info(dispatchable_graph) + self.logger.info("Risk metric: %s ", dispatchable_graph.risk_metric) - completion_time = guide_stn.get_completion_time() - makespan = guide_stn.get_makespan() + completion_time = dispatchable_graph.get_completion_time() + makespan = dispatchable_graph.get_makespan() self.logger.info("Completion time: %s ", completion_time) self.logger.info("Makespan: %s ", makespan) - self.assertEqual(completion_time, 60) + self.assertEqual(completion_time, 163) self.assertEqual(makespan, 97) - expected_alpha = 0.0 - self.assertEqual(alpha, expected_alpha) + expected_risk_metric = 0.0 + self.assertEqual(dispatchable_graph.risk_metric, expected_risk_metric) - constraints = guide_stn.get_constraints() + constraints = dispatchable_graph.get_constraints() for (i, j) in constraints: if i == 0 and j == 1: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 37) self.assertEqual(upper_bound, 38) if i == 0 and j == 2: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 41) self.assertEqual(upper_bound, 47) if i == 0 and j == 3: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 42) self.assertEqual(upper_bound, 54) if i == 0 and j == 4: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 92) self.assertEqual(upper_bound, 94) if i == 0 and j == 5: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 96) self.assertEqual(upper_bound, 102) if i == 0 and j == 6: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 97) self.assertEqual(upper_bound, 109) if i == 1 and j == 2: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 0) self.assertEqual(upper_bound, 47) if i == 2 and j == 3: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 0) self.assertEqual(upper_bound, 61) if i == 3 and j == 4: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 0) self.assertEqual(upper_bound, 61) if i == 4 and j == 5: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 0) self.assertEqual(upper_bound, 61) if i == 5 and j == 6: - lower_bound = -guide_stn[j][i]['weight'] - upper_bound = guide_stn[i][j]['weight'] + lower_bound = -dispatchable_graph[j][i]['weight'] + upper_bound = dispatchable_graph[i][j]['weight'] self.assertEqual(lower_bound, 0) self.assertEqual(upper_bound, MAX_FLOAT) diff --git a/test/update_pstn.py b/test/update_pstn.py index 3ebcc5a..4c48eaf 100644 --- a/test/update_pstn.py +++ b/test/update_pstn.py @@ -1,46 +1,20 @@ +import os from stn.pstn.pstn import PSTN import unittest - - -class Task(object): - - def __init__(self): - self.id = '' - self.earliest_start_time = -1 - self.latest_start_time = -1 - self.start_pose_name = '' - self.finish_pose_name = '' - self.hard_constraints = True +from stn.utils.uuid import from_str +from stn.utils.utils import load_yaml, create_task class UpdatePSTN(unittest.TestCase): def setUp(self): - task_1 = Task() - task_1.id = "616af00-ec3b-4ecd-ae62-c94a3703594c" - task_1.r_earliest_navigation_start_time = 0.0 - task_1.r_earliest_start_time = 96.0 - task_1.r_latest_start_time = 102.0 - task_1.start_pose_name = "AMK_TDU-TGR-1_X_14.03_Y_9.55" - task_1.finish_pose_name = "AMK_TDU-TGR-1_X_15.09_Y_5.69" - - task_2 = Task() - task_2.id = "207cc8da-2f0e-4538-802b-b8f3954df38d" - task_2.r_earliest_navigation_start_time = 0.0 - task_2.r_earliest_start_time = 71.0 - task_2.r_latest_start_time = 76.0 - task_2.start_pose_name = "AMK_TDU-TGR-1_X_7.15_Y_10.55" - task_2.finish_pose_name = "AMK_TDU-TGR-1_X_6.67_Y_14.52" - - task_3 = Task() - task_3.id = "0d06fb90-a76d-48b4-b64f-857b7388ab70" - task_3.r_earliest_navigation_start_time = 0.0 - task_3.r_earliest_start_time = 41.0 - task_3.r_latest_start_time = 47.0 - task_3.start_pose_name = "AMK_TDU-TGR-1_X_9.7_Y_5.6" - task_3.finish_pose_name = "AMK_TDU-TGR-1_X_5.82_Y_6.57" - - self.tasks = [task_1, task_2, task_3] + code_dir = os.path.abspath(os.path.dirname(__file__)) + tasks_dict = load_yaml(code_dir + "/data/tasks.yaml") + self.tasks = list() + for task_dict in tasks_dict.values(): + task = create_task(PSTN(), task_dict) + print(task) + self.tasks.append(task) def test_add_tasks_consecutively(self): """ Adds tasks in consecutive positions. Example diff --git a/test/update_stn.py b/test/update_stn.py index f32188a..8b77752 100644 --- a/test/update_stn.py +++ b/test/update_stn.py @@ -1,46 +1,20 @@ -from stn.stn import STN +import os import unittest - -class Task(object): - - def __init__(self): - self.id = '' - self.earliest_start_time = -1 - self.latest_start_time = -1 - self.start_pose_name = '' - self.finish_pose_name = '' - self.hard_constraints = True +from stn.stn import STN +from stn.utils.utils import load_yaml, create_task class UpdateSTN(unittest.TestCase): def setUp(self): - task_1 = Task() - task_1.id = "616af00-ec3b-4ecd-ae62-c94a3703594c" - task_1.r_earliest_navigation_start_time = 0.0 - task_1.r_earliest_start_time = 96.0 - task_1.r_latest_start_time = 102.0 - task_1.start_pose_name = "AMK_TDU-TGR-1_X_14.03_Y_9.55" - task_1.finish_pose_name = "AMK_TDU-TGR-1_X_15.09_Y_5.69" - - task_2 = Task() - task_2.id = "207cc8da-2f0e-4538-802b-b8f3954df38d" - task_2.r_earliest_navigation_start_time = 0.0 - task_2.r_earliest_start_time = 71.0 - task_2.r_latest_start_time = 76.0 - task_2.start_pose_name = "AMK_TDU-TGR-1_X_7.15_Y_10.55" - task_2.finish_pose_name = "AMK_TDU-TGR-1_X_6.67_Y_14.52" - - task_3 = Task() - task_3.id = "0d06fb90-a76d-48b4-b64f-857b7388ab70" - task_3.r_earliest_navigation_start_time = 0.0 - task_3.r_earliest_start_time = 41.0 - task_3.r_latest_start_time = 47.0 - task_3.start_pose_name = "AMK_TDU-TGR-1_X_9.7_Y_5.6" - task_3.finish_pose_name = "AMK_TDU-TGR-1_X_5.82_Y_6.57" - - self.tasks = [task_1, task_2, task_3] + code_dir = os.path.abspath(os.path.dirname(__file__)) + tasks_dict = load_yaml(code_dir + "/data/tasks.yaml") + self.tasks = list() + for task_dict in tasks_dict.values(): + task = create_task(STN(), task_dict) + print(task) + self.tasks.append(task) def test_add_tasks_consecutively(self): """ Adds tasks in consecutive positions. Example @@ -62,7 +36,7 @@ def test_add_tasks_consecutively(self): self.assertEqual(n_nodes, stn.number_of_nodes()) self.assertEqual(n_edges, stn.number_of_edges()) - def test_add_task_beggining(self): + def test_add_task_beginning(self): """Adds task at the beginning. Displaces the other tasks """ print("--->Adding task at the beginning...") @@ -118,6 +92,8 @@ def test_remove_task_beginning(self): for i, task in enumerate(self.tasks): stn.add_task(task, i+1) + print(stn) + # Remove task in position 1 stn.remove_task(1) @@ -162,7 +138,6 @@ def test_remove_task_end(self): # Add all tasks for i, task in enumerate(self.tasks): stn.add_task(task, i+1) - print(stn) print(stn) # Remove task in position 3 @@ -190,7 +165,7 @@ def test_add_two_tasks(self): print(stn) stn_json = stn.to_json() - print("JSON format", stn_json) + # print("JSON format", stn_json) if __name__ == '__main__': diff --git a/test/update_stnu.py b/test/update_stnu.py index fb34807..879cf39 100644 --- a/test/update_stnu.py +++ b/test/update_stnu.py @@ -1,46 +1,20 @@ -from stn.stnu.stnu import STNU +import os import unittest - -class Task(object): - - def __init__(self): - self.id = '' - self.earliest_start_time = -1 - self.latest_start_time = -1 - self.start_pose_name = '' - self.finish_pose_name = '' - self.hard_constraints = True +from stn.stnu.stnu import STNU +from stn.utils.utils import load_yaml, create_task class UpdateSTNU(unittest.TestCase): def setUp(self): - task_1 = Task() - task_1.id = "616af00-ec3b-4ecd-ae62-c94a3703594c" - task_1.r_earliest_navigation_start_time = 0.0 - task_1.r_earliest_start_time = 96.0 - task_1.r_latest_start_time = 102.0 - task_1.start_pose_name = "AMK_TDU-TGR-1_X_14.03_Y_9.55" - task_1.finish_pose_name = "AMK_TDU-TGR-1_X_15.09_Y_5.69" - - task_2 = Task() - task_2.id = "207cc8da-2f0e-4538-802b-b8f3954df38d" - task_2.r_earliest_navigation_start_time = 0.0 - task_2.r_earliest_start_time = 71.0 - task_2.r_latest_start_time = 76.0 - task_2.start_pose_name = "AMK_TDU-TGR-1_X_7.15_Y_10.55" - task_2.finish_pose_name = "AMK_TDU-TGR-1_X_6.67_Y_14.52" - - task_3 = Task() - task_3.id = "0d06fb90-a76d-48b4-b64f-857b7388ab70" - task_3.r_earliest_navigation_start_time = 0.0 - task_3.r_earliest_start_time = 41.0 - task_3.r_latest_start_time = 47.0 - task_3.start_pose_name = "AMK_TDU-TGR-1_X_9.7_Y_5.6" - task_3.finish_pose_name = "AMK_TDU-TGR-1_X_5.82_Y_6.57" - - self.tasks = [task_1, task_2, task_3] + code_dir = os.path.abspath(os.path.dirname(__file__)) + tasks_dict = load_yaml(code_dir + "/data/tasks.yaml") + self.tasks = list() + for task_dict in tasks_dict.values(): + task = create_task(STNU(), task_dict) + print(task) + self.tasks.append(task) def test_add_tasks_consecutively(self): """ Adds tasks in consecutive positions. Example @@ -118,6 +92,8 @@ def test_remove_task_beginning(self): for i, task in enumerate(self.tasks): stnu.add_task(task, i+1) + print(stnu) + # Remove task in position 1 stnu.remove_task(1) @@ -162,7 +138,6 @@ def test_remove_task_end(self): # Add all tasks for i, task in enumerate(self.tasks): stnu.add_task(task, i+1) - print(stnu) print(stnu) # Remove task in position 3 @@ -190,7 +165,7 @@ def test_add_two_tasks(self): print(stnu) stnu_json = stnu.to_json() - print("JSON format ", stnu_json) + # print("JSON format ", stnu_json) if __name__ == '__main__':