#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ gnpy.core.request ================= This module contains path request functionality. This functionality allows the user to provide a JSON request file in accordance with a Yang model for requesting path computations and returns path results in terms of path and feasibility See: draft-ietf-teas-yang-path-computation-01.txt """ from collections import namedtuple, OrderedDict from logging import getLogger, basicConfig, CRITICAL, DEBUG, INFO from networkx import (dijkstra_path, NetworkXNoPath, all_simple_paths) from networkx.utils import pairwise from numpy import mean from gnpy.core.service_sheet import convert_service_sheet, Request_element, Element from gnpy.core.elements import Transceiver, Roadm, Edfa, Fused from gnpy.core.utils import db2lin, lin2db from gnpy.core.info import create_input_spectral_information, SpectralInformation, Channel, Power from gnpy.core.exceptions import ServiceError, DisjunctionError from copy import copy, deepcopy from csv import writer from math import ceil LOGGER = getLogger(__name__) RequestParams = namedtuple('RequestParams', 'request_id source destination bidir trx_type' + ' trx_mode nodes_list loose_list spacing power nb_channel f_min' + ' f_max format baud_rate OSNR bit_rate roll_off tx_osnr' + ' min_spacing cost path_bandwidth') DisjunctionParams = namedtuple('DisjunctionParams', 'disjunction_id relaxable link' + '_diverse node_diverse disjunctions_req') class Path_request: """ the class that contains all attributes related to a request """ def __init__(self, *args, **params): params = RequestParams(**params) self.request_id = params.request_id self.source = params.source self.destination = params.destination self.bidir = params.bidir self.tsp = params.trx_type self.tsp_mode = params.trx_mode self.baud_rate = params.baud_rate self.nodes_list = params.nodes_list self.loose_list = params.loose_list self.spacing = params.spacing self.power = params.power self.nb_channel = params.nb_channel self.f_min = params.f_min self.f_max = params.f_max self.format = params.format self.OSNR = params.OSNR self.bit_rate = params.bit_rate self.roll_off = params.roll_off self.tx_osnr = params.tx_osnr self.min_spacing = params.min_spacing self.cost = params.cost self.path_bandwidth = params.path_bandwidth def __str__(self): return '\n\t'.join([f'{type(self).__name__} {self.request_id}', f'source: {self.source}', f'destination: {self.destination}']) def __repr__(self): if self.baud_rate is not None: temp = self.baud_rate * 1e-9 temp2 = self.bit_rate * 1e-9 else: temp = self.baud_rate temp2 = self.bit_rate return '\n\t'.join([f'{type(self).__name__} {self.request_id}', f'source: \t{self.source}', f'destination:\t{self.destination}', f'trx type:\t{self.tsp}', f'trx mode:\t{self.tsp_mode}', f'baud_rate:\t{temp} Gbaud', f'bit_rate:\t{temp2} Gb/s', f'spacing:\t{self.spacing * 1e-9} GHz', f'power: \t{round(lin2db(self.power)+30, 2)} dBm', f'nb channels: \t{self.nb_channel}', f'path_bandwidth: \t{round(self.path_bandwidth * 1e-9, 2)} Gbit/s', f'nodes-list:\t{self.nodes_list}', f'loose-list:\t{self.loose_list}' '\n']) class Disjunction: """ the class that contains all attributes related to disjunction constraints """ def __init__(self, *args, **params): params = DisjunctionParams(**params) self.disjunction_id = params.disjunction_id self.relaxable = params.relaxable self.link_diverse = params.link_diverse self.node_diverse = params.node_diverse self.disjunctions_req = params.disjunctions_req def __str__(self): return '\n\t'.join([f'relaxable: {self.relaxable}', f'link-diverse: {self.link_diverse}', f'node-diverse: {self.node_diverse}', f'request-id-numbers: {self.disjunctions_req}']) def __repr__(self): return '\n\t'.join([f'{type(self).__name__} {self.disjunction_id}', f'relaxable: {self.relaxable}', f'link-diverse: {self.link_diverse}', f'node-diverse: {self.node_diverse}', f'request-id-numbers: {self.disjunctions_req}' '\n']) BLOCKING_NOPATH = ['NO_PATH', 'NO_PATH_WITH_CONSTRAINT',\ 'NO_FEASIBLE_BAUDRATE_WITH_SPACING',\ 'NO_COMPUTED_SNR'] BLOCKING_NOMODE = ['NO_FEASIBLE_MODE', 'MODE_NOT_FEASIBLE'] BLOCKING_NOSPECTRUM = 'NO_SPECTRUM' def element_to_node_type(element): if isinstance(element, Transceiver): return "transceiver" if isinstance(element, Edfa): return "EDFA" if isinstance(element, Roadm): return "ROADM" return None class Result_element(Element): def __init__(self, path_request, computed_path, reversed_computed_path=None): self.path_id = path_request.request_id self.path_request = path_request self.computed_path = computed_path # starting implementing reversed properties in case of bidir demand if reversed_computed_path is not None: self.reversed_computed_path = reversed_computed_path uid = property(lambda self: repr(self)) def detailed_path_json(self, path): """ a function that builds path object for normal and blocking cases """ index = 0 pro_list = [] for element in path: temp = { 'path-route-object': { 'index': index, 'num-unnum-hop': { 'node-id': element.uid, 'link-tp-id': element.uid, # TODO change index in order to insert transponder attribute } } } node_type = element_to_node_type(element) if (node_type is not None): temp['path-route-object']['num-unnum-hop']['gnpy-node-type'] = node_type pro_list.append(temp) index += 1 if self.path_request.M > 0: temp = { 'path-route-object': { 'index': index, "label-hop": { "N": self.path_request.N, "M": self.path_request.M }, } } pro_list.append(temp) index += 1 elif self.path_request.M == 0 and hasattr(self.path_request, 'blocking_reason'): # if the path is blocked due to spectrum, no label object is created, but # the json response includes a detailed path for user infromation. pass else: raise ServiceError('request {self.path_id} should have positive path bandwidth value.') if isinstance(element, Transceiver): temp = { 'path-route-object': { 'index': index, 'transponder' : { 'transponder-type' : self.path_request.tsp, 'transponder-mode' : self.path_request.tsp_mode } } } pro_list.append(temp) index += 1 if isinstance(element, Roadm): temp = { 'path-route-object': { 'index': index, 'target-channel-power' : { 'value' : element.effective_pch_out_db, } } } pro_list.append(temp) index += 1 if isinstance(element, Edfa): temp = { 'path-route-object': { 'index': index, 'target-channel-power' : { 'value': element.effective_pch_out_db, }, 'output-voa': { 'value': element.out_voa, } } } pro_list.append(temp) index += 1 return pro_list @property def path_properties(self): """ a function that returns the path properties (metrics, crossed elements) into a dict """ def path_metric(pth, req): """ creates the metrics dictionary """ return [ { 'metric-type': 'SNR-bandwidth', 'accumulative-value': round(mean(pth[-1].snr), 2) }, { 'metric-type': 'SNR-0.1nm', 'accumulative-value': round(mean(pth[-1].snr+lin2db(req.baud_rate/12.5e9)), 2) }, { 'metric-type': 'OSNR-bandwidth', 'accumulative-value': round(mean(pth[-1].osnr_ase), 2) }, { 'metric-type': 'OSNR-0.1nm', 'accumulative-value': round(mean(pth[-1].osnr_ase_01nm), 2) }, { 'metric-type': 'reference_power', 'accumulative-value': req.power }, { 'metric-type': 'path_bandwidth', 'accumulative-value': req.path_bandwidth } ] if self.path_request.bidir: path_properties = { 'path-metric': path_metric(self.computed_path, self.path_request), 'z-a-path-metric': path_metric(self.reversed_computed_path, self.path_request), 'path-route-objects': self.detailed_path_json(self.computed_path), 'reversed-path-route-objects': self.detailed_path_json(self.reversed_computed_path), } else: path_properties = { 'path-metric': path_metric(self.computed_path, self.path_request), 'path-route-objects': self.detailed_path_json(self.computed_path) } return path_properties @property def pathresult(self): """ create the result dictionnary (response for a request) """ try: if self.path_request.blocking_reason in BLOCKING_NOPATH: response = { 'response-id': self.path_id, 'no-path': { 'no-path': self.path_request.blocking_reason } } return response else: response = { 'response-id': self.path_id, 'no-path': { 'no-path': self.path_request.blocking_reason, 'path-properties': self.path_properties } } return response except AttributeError: response = { 'response-id': self.path_id, 'path-properties': self.path_properties } return response @property def json(self): return self.pathresult def compute_constrained_path(network, req): trx = [n for n in network.nodes() if isinstance(n, Transceiver)] roadm = [n for n in network.nodes() if isinstance(n, Roadm)] edfa = [n for n in network.nodes() if isinstance(n, Edfa)] anytypenode = [n for n in network.nodes()] source = next(el for el in trx if el.uid == req.source) # This method ensures that the constraint can be satisfied without loops # except when it is not possible: eg if constraints makes a loop # It requires that the source, dest and nodes are correct (no error in the names) destination = next(el for el in trx if el.uid == req.destination) nodes_list = [] for n_elem in req.nodes_list: # for debug excel print(n) nodes_list.append(next(el for el in anytypenode if el.uid == n_elem)) # nodes_list contains at least the destination if nodes_list is None: # only arrive here if there is a bug in the program because route lists have # been corrected and harmonized before msg = f'Request {req.request_id} problem in the constitution of nodes_list: ' +\ 'should at least include destination' LOGGER.critical(msg) raise ValueError(msg) if req.nodes_list[-1] != req.destination: # only arrive here if there is a bug in the program because route lists have # been corrected and harmonized before msg = f'Request {req.request_id} malformed list of nodes: last node should '+\ 'be destination trx' LOGGER.critical(msg) raise ValueError() if len(nodes_list) == 1: try: total_path = dijkstra_path(network, source, destination, weight='weight') # print('checking edges length is correct') # print(shortest_path_length(network,source,destination)) # print(shortest_path_length(network,source,destination,weight ='weight')) # s = total_path[0] # for e in total_path[1:]: # print(s.uid) # print(network.get_edge_data(s,e)) # s = e except NetworkXNoPath: msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path from' +\ f' {source.uid} to node: {destination.uid} in network topology'+ '\x1b[0m' LOGGER.critical(msg) print(msg) req.blocking_reason = 'NO_PATH' total_path = [] else: all_simp_pths = list(all_simple_paths(network, source=source,\ target=destination, cutoff=120)) candidate = [] for pth in all_simp_pths: if ispart(nodes_list, pth): # print(f'selection{[el.uid for el in p if el in roadm]}') candidate.append(pth) # select the shortest path (in nb of hops) -> changed to shortest path in km length if len(candidate) > 0: # candidate.sort(key=lambda x: len(x)) candidate.sort(key=lambda x: sum(network.get_edge_data(x[i], x[i+1])['weight']\ for i in range(len(x)-2))) total_path = candidate[0] else: # TODO: better account for individual loose and strict node # to ease: suppose that one strict makes the whole liste strict (except for the # last node which is the transceiver) # if all nodes i n node_list are LOOSE constraint, skip the constraints and find # a path w/o constraints, else there is no possible path if nodes_list[:-len("STRICT")]: print(f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path crossing ' +\ f'{[el.uid for el in nodes_list[:-len("STRICT")]]} in network topology'+ '\x1b[0m') else: print(f'\x1b[1;33;40m'+f'User include_node constraints could not be applied ' +\ f'(invalid names specified)'+ '\x1b[0m') if 'STRICT' not in req.loose_list[:-len('STRICT')]: msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path with user_' +\ f'include node constraints' + '\x1b[0m' LOGGER.info(msg) print(f'constraint ignored') total_path = dijkstra_path(network, source, destination, weight='weight') else: msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path with user ' +\ f'include node constraints.\nNo path computed'+ '\x1b[0m' LOGGER.critical(msg) print(msg) req.blocking_reason = 'NO_PATH_WITH_CONSTRAINT' total_path = [] # the following method was initially used but abandonned: compute per segment: # this does not guaranty to avoid loops or correct results # Here is the demonstration: # 1 1 # eg a----b-----c # |1 |0.5 |1 # e----f--h--g # 1 0.5 0.5 # if I have to compute a to g with constraint f-c # result will be a concatenation of: a-b-f and f-b-c and c-g # which means a loop. # if to avoid loops I iteratively suppress edges of the segments in the topo # segment 1 = a-b-f # 1 # eg a b-----c # |1 |1 # e----f--h--g # 1 0.5 0.5 # then # segment 2 = f-h-g-c # 1 # eg a b-----c # |1 # e----f h g # 1 # then there is no more path to g destination return total_path def propagate(path, req, equipment): si = create_input_spectral_information( req.f_min, req.f_max, req.roll_off, req.baud_rate, req.power, req.spacing) for i, el in enumerate(path): if isinstance(el, Roadm): next_el = path[i+1] si = el(si, degree=next_el.uid) else: si = el(si) print(el) path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr) return path def propagate2(path, req, equipment): si = create_input_spectral_information( req.f_min, req.f_max, req.roll_off, req.baud_rate, req.power, req.spacing) infos = {} for i, el in enumerate(path): before_si = si if isinstance(el, Roadm): next_el = path[i+1] after_si = si = el(si, degree=next_el.uid) else: after_si = si = el(si) infos[el] = before_si, after_si path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr) return infos def propagate_and_optimize_mode(path, req, equipment): # if mode is unknown : loops on the modes starting from the highest baudrate fiting in the # step 1: create an ordered list of modes based on baudrate baudrate_to_explore = list(set([this_mode['baud_rate'] for this_mode in equipment['Transceiver'][req.tsp].mode if float(this_mode['min_spacing']) <= req.spacing])) # TODO be carefull on limits cases if spacing very close to req spacing eg 50.001 50.000 baudrate_to_explore = sorted(baudrate_to_explore, reverse=True) if baudrate_to_explore: # at least 1 baudrate can be tested wrt spacing for this_br in baudrate_to_explore: modes_to_explore = [this_mode for this_mode in equipment['Transceiver'][req.tsp].mode if this_mode['baud_rate'] == this_br and float(this_mode['min_spacing']) <= req.spacing] modes_to_explore = sorted(modes_to_explore, key=lambda x: x['bit_rate'], reverse=True) # print(modes_to_explore) # step2: computes propagation for each baudrate: stop and select the first that passes found_a_feasible_mode = False # TODO: the case of roll of is not included: for now use SI one # TODO: if the loop in mode optimization does not have a feasible path, then bugs spc_info = create_input_spectral_information(req.f_min, req.f_max, equipment['SI']['default'].roll_off, this_br, req.power, req.spacing) for i, el in enumerate(path): if isinstance(el, Roadm): next_el = path[i+1] spc_info = el(spc_info, degree=next_el.uid) else: spc_info = el(spc_info) for this_mode in modes_to_explore: if path[-1].snr is not None: path[-1].update_snr(this_mode['tx_osnr'], equipment['Roadm']['default'].add_drop_osnr) if round(min(path[-1].snr+lin2db(this_br/(12.5e9))), 2) > this_mode['OSNR']: found_a_feasible_mode = True return path, this_mode else: last_explored_mode = this_mode else: req.blocking_reason = 'NO_COMPUTED_SNR' return path, None # only get to this point if no baudrate/mode satisfies OSNR requirement # returns the last propagated path and mode msg = f'\tWarning! Request {req.request_id}: no mode satisfies path SNR requirement.\n' print(msg) LOGGER.info(msg) req.blocking_reason = 'NO_FEASIBLE_MODE' return path, last_explored_mode else: # no baudrate satisfying spacing msg = f'\tWarning! Request {req.request_id}: no baudrate satisfies spacing requirement.\n' print(msg) LOGGER.info(msg) req.blocking_reason = 'NO_FEASIBLE_BAUDRATE_WITH_SPACING' return [], None def jsontopath_metric(path_metric): """ a functions that reads resulting metric from json string """ output_snr = next(e['accumulative-value'] for e in path_metric if e['metric-type'] == 'SNR-0.1nm') output_snrbandwidth = next(e['accumulative-value'] for e in path_metric if e['metric-type'] == 'SNR-bandwidth') output_osnr = next(e['accumulative-value'] for e in path_metric if e['metric-type'] == 'OSNR-0.1nm') # ouput osnr@bandwidth is not used # output_osnrbandwidth = next(e['accumulative-value'] # for e in path_metric if e['metric-type'] == 'OSNR-bandwidth') power = next(e['accumulative-value'] for e in path_metric if e['metric-type'] == 'reference_power') path_bandwidth = next(e['accumulative-value'] for e in path_metric if e['metric-type'] == 'path_bandwidth') return output_snr, output_snrbandwidth, output_osnr, power, path_bandwidth def jsontoparams(my_p, tsp, mode, equipment): """ a function that derives optical params from transponder type and mode supports the no mode case """ temp = [] for elem in my_p['path-properties']['path-route-objects']: if 'num-unnum-hop' in elem['path-route-object']: temp.append(elem['path-route-object']['num-unnum-hop']['node-id']) pth = ' | '.join(temp) temp2 = [] for elem in my_p['path-properties']['path-route-objects']: if 'label-hop' in elem['path-route-object'].keys(): temp2.append(f'{elem["path-route-object"]["label-hop"]["N"]}, ' + \ f'{elem["path-route-object"]["label-hop"]["M"]}') # OrderedDict.fromkeys returns the unique set of strings. # TODO: if spectrum changes along the path, we should be able to give the segments # eg for regeneration case temp2 = list(OrderedDict.fromkeys(temp2)) sptrm = ' | '.join(temp2) # find the tsp minOSNR, baud rate... from the eqpt library based # on tsp (type) and mode (format). # loading equipment already tests the existence of tsp type and mode: if mode is not None: [minosnr, baud_rate, bit_rate, cost] = \ next([m['OSNR'], m['baud_rate'], m['bit_rate'], m['cost']] for m in equipment['Transceiver'][tsp].mode if m['format'] == mode) else: [minosnr, baud_rate, bit_rate, cost] = ['', '', '', ''] output_snr, output_snrbandwidth, output_osnr, power, path_bandwidth = \ jsontopath_metric(my_p['path-properties']['path-metric']) return pth, minosnr, baud_rate, bit_rate, cost, output_snr, \ output_snrbandwidth, output_osnr, power, path_bandwidth, sptrm def jsontocsv(json_data, equipment, fileout): """ reads json path result file in accordance with: Yang model for requesting Path Computation draft-ietf-teas-yang-path-computation-01.txt. and write results in an CSV file """ mywriter = writer(fileout) mywriter.writerow(('response-id', 'source', 'destination', 'path_bandwidth', 'Pass?',\ 'nb of tsp pairs', 'total cost', 'transponder-type', 'transponder-mode',\ 'OSNR-0.1nm', 'SNR-0.1nm', 'SNR-bandwidth', 'baud rate (Gbaud)',\ 'input power (dBm)', 'path', 'spectrum (N,M)', 'reversed path OSNR-0.1nm',\ 'reversed path SNR-0.1nm', 'reversed path SNR-bandwidth')) for pth_el in json_data['response']: path_id = pth_el['response-id'] if 'no-path' in pth_el.keys(): total_cost = '' nb_tsp = '' sptrm = '' if pth_el['no-path']['no-path'] in BLOCKING_NOPATH: source = '' destination = '' pthbdbw = '' isok = pth_el['no-path']['no-path'] tsp = '' mode = '' rosnr = '' rsnr = '' rsnrb = '' brate = '' pwr = '' pth = '' revosnr = '' revsnr = '' revsnrb = '' else: # the objects are listed with this order: # - id of hop # - label (N,M) # - transponder for source and destination only # as spectrum assignment is not performed for blocked demands: there is no label object in the answer # so the hop_attribute with tsp and mode is second object or last object, while id of hop is first and # penultimate source = pth_el['no-path']['path-properties']['path-route-objects'][0]\ ['path-route-object']['num-unnum-hop']['node-id'] destination = pth_el['no-path']['path-properties']['path-route-objects'][-2]\ ['path-route-object']['num-unnum-hop']['node-id'] temp_tsp = pth_el['no-path']['path-properties']['path-route-objects'][1]\ ['path-route-object']['transponder'] tsp = temp_tsp['transponder-type'] mode = temp_tsp['transponder-mode'] isok = pth_el['no-path']['no-path'] if pth_el['no-path']['no-path'] in BLOCKING_NOMODE or \ pth_el['no-path']['no-path'] in BLOCKING_NOSPECTRUM: pth, minosnr, baud_rate, bit_rate, cost, output_snr, output_snrbandwidth, \ output_osnr, power, path_bandwidth, sptrm = \ jsontoparams(pth_el['no-path'], tsp, mode, equipment) pthbdbw = '' rosnr = round(output_osnr, 2) rsnr = round(output_snr, 2) rsnrb = round(output_snrbandwidth, 2) brate = round(baud_rate * 1e-9, 2) pwr = round(lin2db(power) + 30, 2) if 'z-a-path-metric' in pth_el['no-path']['path-properties'].keys(): output_snr, output_snrbandwidth, output_osnr, power, path_bandwidth = \ jsontopath_metric(pth_el['no-path']['path-properties']['z-a-path-metric']) revosnr = round(output_osnr, 2) revsnr = round(output_snr, 2) revsnrb = round(output_snrbandwidth, 2) else: revosnr = '' revsnr = '' revsnrb = '' else: # when label will be assigned destination will be with index -3, and transponder with index 2 source = pth_el['path-properties']['path-route-objects'][0]\ ['path-route-object']['num-unnum-hop']['node-id'] destination = pth_el['path-properties']['path-route-objects'][-3]\ ['path-route-object']['num-unnum-hop']['node-id'] # selects only roadm nodes temp_tsp = pth_el['path-properties']['path-route-objects'][2]\ ['path-route-object']['transponder'] tsp = temp_tsp['transponder-type'] mode = temp_tsp['transponder-mode'] # find the min acceptable OSNR, baud rate from the eqpt library based # on tsp (type) and mode (format). # loading equipment already tests the existence of tsp type and mode: pth, minosnr, baud_rate, bit_rate, cost, output_snr, output_snrbandwidth, \ output_osnr, power, path_bandwidth, sptrm = \ jsontoparams(pth_el, tsp, mode, equipment) # this part only works if the request has a blocking_reason atribute, ie if it could not be satisfied isok = output_snr >= minosnr nb_tsp = ceil(path_bandwidth / bit_rate) pthbdbw = round(path_bandwidth * 1e-9, 2) rosnr = round(output_osnr, 2) rsnr = round(output_snr, 2) rsnrb = round(output_snrbandwidth, 2) brate = round(baud_rate * 1e-9, 2) pwr = round(lin2db(power) + 30, 2) total_cost = nb_tsp * cost if 'z-a-path-metric' in pth_el['path-properties'].keys(): output_snr, output_snrbandwidth, output_osnr, power, path_bandwidth = \ jsontopath_metric(pth_el['path-properties']['z-a-path-metric']) revosnr = round(output_osnr, 2) revsnr = round(output_snr, 2) revsnrb = round(output_snrbandwidth, 2) else: revosnr = '' revsnr = '' revsnrb = '' mywriter.writerow((path_id, source, destination, pthbdbw, isok, nb_tsp, total_cost, tsp, mode, rosnr, rsnr, rsnrb, brate, pwr, pth, sptrm, revosnr, revsnr, revsnrb )) def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list): # pathreqlist is a list of Path_request objects # disjunctions_list a list of Disjunction objects # given a network, a list of requests with the set of disjunction features between # request, the function computes the set of path satisfying: first the disjunction # constraint and second the routing constraint if the request include an explicit # set of elements to pass through. # the algorithm used allows to specify disjunction for demands not sharing source or # destination. # a request might be declared as disjoint from several requests # it is a iterative process: # first computes a list of all shortest path (this may add computation time) # second elaborate the set of path solution for each synchronization vector # third select only the candidates that satisfy all synchronization vectors they belong to # fourth apply route constraints: remove candidate path that do not satisfy the constraint # fifth select the first candidate among the set of candidates. # the example network used in comments has been added to the set of data tests files # define the list to be returned path_res_list = [] # all disjctn must be computed at once together to avoid blocking # 1 1 # eg a----b-----c # |1 |0.5 |1 # e----f--h--g # 1 0.5 0.5 # if I have to compute a to g and a to h # I must not compute a-b-f-h-g, otherwise there is no disjoint path remaining for a to h # instead I should list all most disjoint path and select the one that have the less # number of commonalities # \ path abfh aefh abcgh # \___cost 2 2.5 3.5 # path| cost # abfhg| 2.5 x x # abcg | 3 x x # aefhg| 3 x x x # from this table abcg and aefh have no common links and should be preferred # even they are not the shortest paths # build the list of pathreqlist elements not concerned by disjunction global_disjunctions_list = [e for d in disjunctions_list for e in d.disjunctions_req] pathreqlist_simple = [e for e in pathreqlist if e.request_id not in global_disjunctions_list] pathreqlist_disjt = [e for e in pathreqlist if e.request_id in global_disjunctions_list] # use a mirror class to record path and the corresponding requests class Pth: def __init__(self, req, pth, simplepth): self.req = req self.pth = pth self.simplepth = simplepth # step 1 # for each remaining request compute a set of simple path allpaths = {} rqs = {} simple_rqs = {} simple_rqs_reversed = {} for pathreq in pathreqlist_disjt: all_simp_pths = list(all_simple_paths(network,\ source=next(el for el in network.nodes() if el.uid == pathreq.source),\ target=next(el for el in network.nodes() if el.uid == pathreq.destination),\ cutoff=80)) # sort them in km length instead of hop # all_simp_pths = sorted(all_simp_pths, key=lambda path: len(path)) all_simp_pths = sorted(all_simp_pths, key=lambda \ x: sum(network.get_edge_data(x[i], x[i+1])['weight'] for i in range(len(x)-2))) # reversed direction paths required to check disjunction on both direction all_simp_pths_reversed = [] for pth in all_simp_pths: all_simp_pths_reversed.append(find_reversed_path(pth)) rqs[pathreq.request_id] = all_simp_pths temp = [] for pth in all_simp_pths: # build a short list representing each roadm+direction with the first item # start enumeration at 1 to avoid Trx in the list short_list = [e.uid for i, e in enumerate(pth[1:-1]) \ if isinstance(e, Roadm) | (isinstance(pth[i], Roadm))] temp.append(short_list) # id(short_list) is unique even if path is the same: two objects with same # path have two different ids allpaths[id(short_list)] = Pth(pathreq, pth, short_list) simple_rqs[pathreq.request_id] = temp temp = [] for pth in all_simp_pths_reversed: # build a short list representing each roadm+direction with the first item # start enumeration at 1 to avoid Trx in the list temp.append([e.uid for i, e in enumerate(pth[1:-1]) \ if isinstance(e, Roadm) | (isinstance(pth[i], Roadm))]) simple_rqs_reversed[pathreq.request_id] = temp # step 2 # for each set of requests that need to be disjoint # select the disjoint path combination candidates = {} for d in disjunctions_list: dlist = d.disjunctions_req.copy() # each line of dpath is one combination of path that satisfies disjunction dpath = [] for i, pth in enumerate(simple_rqs[dlist[0]]): dpath.append([pth]) # allpaths[id(p)].d_id = d.disjunction_id # in each loop, dpath is updated with a path for rq that satisfies # disjunction with each path in dpath # for example, assume set of requests in the vector (disjunction_list) is {rq1,rq2, rq3} # rq1 p1: abfhg # p2: aefhg # p3: abcg # rq2 p8: bf # rq3 p4: abcgh # p6: aefh # p7: abfh # initiate with rq1 # dpath = [[p1] # [p2] # [p3]] # after first loop: # dpath = [[p1 p8] # [p3 p8]] # since p2 and p8 are not disjoint # after second loop: # dpath = [ p3 p8 p6 ] # since p1 and p4 are not disjoint # p1 and p7 are not disjoint # p3 and p4 are not disjoint # p3 and p7 are not disjoint for elem1 in dlist[1:]: temp = [] for j, pth1 in enumerate(simple_rqs[elem1]): # can use index j in simple_rqs_reversed because index # of direct and reversed paths have been kept identical pth1_reversed = simple_rqs_reversed[elem1][j] # print(pth1_reversed) # print('\n\n') for cndt in dpath: # print(f' c: \t{c}') temp2 = cndt.copy() all_disjoint = 0 for pth in cndt: all_disjoint += isdisjoint(pth1, pth) + isdisjoint(pth1_reversed, pth) if all_disjoint == 0: temp2.append(pth1) temp.append(temp2) # print(f' coucou {elem1}: \t{temp}') dpath = temp # print(dpath) candidates[d.disjunction_id] = dpath # for i in disjunctions_list: # print(f'\n{candidates[i.disjunction_id]}') # step 3 # now for each request, select the path that satisfies all disjunctions # path must be in candidates[id] for all concerned ids # for example, assume set of sync vectors (disjunction groups) is # s1 = {rq1 rq2} s2 = {rq1 rq3} # candidate[s1] = [[p1 p8] # [p3 p8]] # candidate[s2] = [[p3 p6]] # for rq1 p3 should be preferred for pathreq in pathreqlist_disjt: concerned_d_id = [d.disjunction_id for d in disjunctions_list if pathreq.request_id in d.disjunctions_req] # for each set of solution, verify that the same path is used for the same request candidate_paths = simple_rqs[pathreq.request_id] # print('coucou') # print(pathreq.request_id) for pth in candidate_paths: iscandidate = 0 for sol in concerned_d_id: test = 1 # for each solution test if pth is part of the solution # if yes, then pth can remain a candidate for cndt in candidates[sol]: if pth in cndt: if allpaths[id(cndt[cndt.index(pth)])].req.request_id == pathreq.request_id: test = 0 break iscandidate += test if iscandidate != 0: for this_id in concerned_d_id: for cndt in candidates[this_id]: if pth in cndt: candidates[this_id].remove(cndt) # for i in disjunctions_list: # print(i.disjunction_id) # print(f'\n{candidates[i.disjunction_id]}') # step 4 apply route constraints: remove candidate path that do not satisfy # the constraint only in the case of disjounction: the simple path is processed in # request.compute_constrained_path # TODO: keep a version without the loose constraint for this_d in disjunctions_list: temp = [] for j, sol in enumerate(candidates[this_d.disjunction_id]): testispartok = True for pth in sol: # print(f'test {allpaths[id(pth)].req.request_id}') # print(f'length of route {len(allpaths[id(pth)].req.nodes_list)}') if allpaths[id(pth)].req.nodes_list: # if pth does not containt the ordered list node, remove sol from the candidate # except if this was the last solution: then check if the constraint is loose # or not if not ispart(allpaths[id(pth)].req.nodes_list, pth): # print(f'nb of solutions {len(temp)}') if j < len(candidates[this_d.disjunction_id])-1: msg = f'removing {sol}' LOGGER.info(msg) testispartok = False #break else: if 'LOOSE' in allpaths[id(pth)].req.loose_list: LOGGER.info(f'Could not apply route constraint'+ f'{allpaths[id(pth)].req.nodes_list} on request' +\ f' {allpaths[id(pth)].req.request_id}') else: LOGGER.info(f'removing last solution from candidate paths\n{sol}') testispartok = False if testispartok: temp.append(sol) candidates[this_d.disjunction_id] = temp # step 5 select the first combination that works pathreslist_disjoint = {} for dis in disjunctions_list: test_sol = True while test_sol: # print('coucou') if candidates[dis.disjunction_id]: for pth in candidates[dis.disjunction_id][0]: if allpaths[id(pth)].req in pathreqlist_disjt: # print(f'selected path:{pth} for req {allpaths[id(pth)].req.request_id}') pathreslist_disjoint[allpaths[id(pth)].req] = allpaths[id(pth)].pth pathreqlist_disjt.remove(allpaths[id(pth)].req) candidates = remove_candidate(candidates, allpaths, allpaths[id(pth)].req, pth) test_sol = False else: msg = f'No disjoint path found with added constraint' LOGGER.critical(msg) print(f'{msg}\nComputation stopped.') # TODO in this case: replay step 5 with the candidate without constraints raise DisjunctionError(msg) # for i in disjunctions_list: # print(i.disjunction_id) # print(f'\n{candidates[i.disjunction_id]}') # list the results in the same order as initial pathreqlist for req in pathreqlist: req.nodes_list.append(req.destination) # we assume that the destination is a strict constraint req.loose_list.append('STRICT') if req in pathreqlist_simple: path_res_list.append(compute_constrained_path(network, req)) else: path_res_list.append(pathreslist_disjoint[req]) return path_res_list def isdisjoint(pth1, pth2): """ returns 0 if disjoint """ edge1 = list(pairwise(pth1)) edge2 = list(pairwise(pth2)) for edge in edge1: if edge in edge2: return 1 return 0 def find_reversed_path(pth): """ select of intermediate roadms and find the path between them note that this function may not give an exact result in case of multiple links between two adjacent nodes. """ # TODO add some indication on elements to indicate from which other they # are the reversed direction. This is partly done with oms indication # we want the list of crossed oms and each item must be unique in the list: # since a succession of elements of the path can be in the same oms, a 'unique' # function is needed # the OrderedDict.fromkeys function does this. eg # pth = [el1_oms1 el2_oms1 el3_oms1 el1_oms2 el2_oms2 el3_oms2] # p_oms should be = [oms1 oms2] p_oms = list(OrderedDict.fromkeys(reversed([el.oms.reversed_oms for el in pth \ if not isinstance(el, Transceiver) and not isinstance(el, Roadm)]))) reversed_path = [pth[-1]] for oms in p_oms: if oms is not None: reversed_path.extend(oms.el_list) # similarly each oms starts and ends with a roadm so roadm may be repeated # if we don't use the OrderedDict.fromkeys function. eg: # if oms1 = [roadma el1 el2 roadmb] and oms2 = [roadmb el3 el4 roadmc] # concatenation should be [roadma el1 el2 roadmb el3 el4 roadmc] reversed_path = list(OrderedDict.fromkeys(reversed_path)) else: msg = f'Error while handling reversed path {pth[-1].uid} to {pth[0].uid}:' +\ ' can not handle unidir topology. TO DO.' LOGGER.critical(msg) raise ValueError(msg) reversed_path.append(pth[0]) return reversed_path def ispart(ptha, pthb): """ the functions takes two paths a and b and retrns True if all a elements are part of b and in the same order """ j = 0 for elem in ptha: if elem in pthb: if pthb.index(elem) >= j: j = pthb.index(elem) else: return False else: return False return True def remove_candidate(candidates, allpaths, rqst, pth): """ filter duplicate candidates """ # print(f'coucou {rqst.request_id}') for key, candidate in candidates.items(): temp = candidate.copy() for sol in candidate: for this_p in sol: if allpaths[id(this_p)].req.request_id == rqst.request_id: if id(this_p) != id(pth): temp.remove(sol) break candidates[key] = temp return candidates def compare_reqs(req1, req2, disjlist): """ compare two requests: returns True or False """ dis1 = [d for d in disjlist if req1.request_id in d.disjunctions_req] dis2 = [d for d in disjlist if req2.request_id in d.disjunctions_req] same_disj = False if dis1 and dis2: temp1 = [] for this_d in dis1: temp1.extend(this_d.disjunctions_req) temp1.remove(req1.request_id) temp2 = [] for this_d in dis2: temp2.extend(this_d.disjunctions_req) temp2.remove(req2.request_id) if set(temp1) == set(temp2): same_disj = True elif not dis2 and not dis1: same_disj = True if req1.source == req2.source and \ req1.destination == req2.destination and \ req1.tsp == req2.tsp and \ req1.tsp_mode == req2.tsp_mode and \ req1.baud_rate == req2.baud_rate and \ req1.nodes_list == req2.nodes_list and \ req1.loose_list == req2.loose_list and \ req1.spacing == req2.spacing and \ req1.power == req2.power and \ req1.nb_channel == req2.nb_channel and \ req1.f_min == req2.f_min and \ req1.f_max == req2.f_max and \ req1.format == req2.format and \ req1.OSNR == req2.OSNR and \ req1.roll_off == req2.roll_off and \ same_disj: return True else: return False def requests_aggregation(pathreqlist, disjlist): """ this function aggregates requests so that if several requests exist between same source and destination and with same transponder type """ # todo maybe add conditions on mode ??, spacing ... # currently if undefined takes the default values local_list = pathreqlist.copy() for req in pathreqlist: for this_r in local_list: if req.request_id != this_r.request_id and compare_reqs(req, this_r, disjlist): # aggregate this_r.path_bandwidth += req.path_bandwidth temp_r_id = this_r.request_id this_r.request_id = ' | '.join((this_r.request_id, req.request_id)) # remove request from list local_list.remove(req) # todo change also disjunction req with new demand for this_d in disjlist: if req.request_id in this_d.disjunctions_req: this_d.disjunctions_req.remove(req.request_id) this_d.disjunctions_req.append(this_r.request_id) for this_d in disjlist: if temp_r_id in this_d.disjunctions_req: disjlist.remove(this_d) break return local_list, disjlist