def consistency_check(params, f_max_from_si): """ checks that the requested parameters are consistant (spacing vs nb channel, vs transponder mode...) """ f_min = params['f_min'] f_max = params['f_max'] max_recommanded_nb_channels = automatic_nch(f_min, f_max, params['spacing']) if params['baud_rate'] is not None: #implicitely means that a mode is defined with min_spacing if params['min_spacing'] > params['spacing']: msg = f'Request {params["request_id"]} has spacing below transponder ' +\ f'{params["trx_type"]} {params["trx_mode"]} min spacing value ' +\ f'{params["min_spacing"]*1e-9}GHz.\nComputation stopped' print(msg) LOGGER.critical(msg) raise ServiceError(msg) if f_max > f_max_from_si: msg = dedent(f''' Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"]} GHz and requested spacing {params["spacing"]*1e-9}GHz is not consistent with frequency range {f_min*1e-12} THz, {f_max*1e-12} THz, min recommanded spacing {params["min_spacing"]*1e-9}GHz. max recommanded nb of channels is {max_recommanded_nb_channels} Computation stopped.''') LOGGER.critical(msg) raise ServiceError(msg)
def correct_route_list(network, pathreqlist): """ prepares the format of route list of nodes to be consistant remove wrong names, remove endpoints also correct source and destination """ anytype = [n.uid for n in network.nodes()] # TODO there is a problem of identification of fibers in case of parallel fibers # between two adjacent roadms so fiber constraint is not supported transponders = [ n.uid for n in network.nodes() if isinstance(n, Transceiver) ] for pathreq in pathreqlist: for i, n_id in enumerate(pathreq.nodes_list): # replace possibly wrong name with a formated roadm name # print(n_id) if n_id not in anytype: # find nodes name that include constraint among all possible names except # transponders (not yet supported as constraints). nodes_suggestion = [uid for uid in anytype \ if n_id.lower() in uid.lower() and uid not in transponders] if pathreq.loose_list[i] == 'LOOSE': if len(nodes_suggestion) > 0: new_n = nodes_suggestion[0] print(f'invalid route node specified:\ \n\'{n_id}\', replaced with \'{new_n}\'') pathreq.nodes_list[i] = new_n else: print(f'\x1b[1;33;40m'+f'invalid route node specified \'{n_id}\',' +\ f' could not use it as constraint, skipped!'+'\x1b[0m') pathreq.nodes_list.remove(n_id) pathreq.loose_list.pop(i) else: msg = f'\x1b[1;33;40m'+f'could not find node: {n_id} in network topology.' +\ f' Strict constraint can not be applied.' + '\x1b[0m' LOGGER.critical(msg) raise ValueError(msg) if pathreq.source not in transponders: msg = f'\x1b[1;31;40m' + f'Request: {pathreq.request_id}: could not find' +\ f' transponder source: {pathreq.source}.'+'\x1b[0m' LOGGER.critical(msg) print(f'{msg}\nComputation stopped.') raise ServiceError(msg) if pathreq.destination not in transponders: msg = f'\x1b[1;31;40m'+f'Request: {pathreq.request_id}: could not find' +\ f' transponder destination: {pathreq.destination}.'+'\x1b[0m' LOGGER.critical(msg) print(f'{msg}\nComputation stopped.') raise ServiceError(msg) # TODO remove endpoints from this list in case they were added by the user # in the xls or json files return pathreqlist
def select_candidate(candidates, policy): """ selects a candidate among all available spectrum """ if policy == 'first_fit': if candidates: return candidates[0] else: return (None, None, None) else: raise ServiceError('Only first_fit spectrum assignment policy is implemented.')
def detailed_path_json(self): """ a function that builds path object for normal and blocking cases """ index = 0 pro_list = [] for element in self.computed_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 } } } 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 return pro_list
def _check_one_request(params, f_max_from_si): """Checks that the requested parameters are consistant (spacing vs nb channel vs transponder mode...)""" f_min = params['f_min'] f_max = params['f_max'] max_recommanded_nb_channels = automatic_nch(f_min, f_max, params['spacing']) if params['baud_rate'] is not None: # implicitly means that a mode is defined with min_spacing if params['min_spacing'] > params['spacing']: msg = f'Request {params["request_id"]} has spacing below transponder ' +\ f'{params["trx_type"]} {params["trx_mode"]} min spacing value ' +\ f'{params["min_spacing"]*1e-9}GHz.\nComputation stopped' print(msg) _logger.critical(msg) raise ServiceError(msg) if f_max > f_max_from_si: msg = f'''Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"]} GHz and requested spacing {params["spacing"]*1e-9}GHz is not consistent with frequency range {f_min*1e-12} THz, {f_max*1e-12} THz, min recommanded spacing {params["min_spacing"]*1e-9}GHz. max recommanded nb of channels is {max_recommanded_nb_channels}.''' _logger.critical(msg) raise ServiceError(msg) # Transponder mode already selected; will it fit to the requested bandwidth? if params['trx_mode'] is not None and params['effective_freq_slot'] is not None \ and params['effective_freq_slot']['M'] is not None: _, requested_m = compute_spectrum_slot_vs_bandwidth( params['path_bandwidth'], params['spacing'], params['bit_rate']) # params['effective_freq_slot']['M'] value should be bigger than the computed requested_m (simple estimate) # TODO: elaborate a more accurate estimate with nb_wl * tx_osnr + possibly guardbands in case of # superchannel closed packing. if requested_m > params['effective_freq_slot']['M']: msg = f'requested M {params["effective_freq_slot"]["M"]} number of slots for request' +\ f'{params["request_id"]} should be greater than {requested_m} to support request' +\ f'{params["path_bandwidth"] * 1e-9} Gbit/s with {params["trx_type"]} {params["trx_mode"]}' _logger.critical(msg) raise ServiceError(msg)
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
def __init__(self, Request, equipment, bidir): # request_id is str # excel has automatic number formatting that adds .0 on integer values # the next lines recover the pure int value, assuming this .0 is unwanted self.request_id = correct_xlrd_int_to_str_reading(Request.request_id) self.source = f'trx {Request.source}' self.destination = f'trx {Request.destination}' # TODO: the automatic naming generated by excel parser requires that source and dest name # be a string starting with 'trx' : this is manually added here. self.srctpid = f'trx {Request.source}' self.dsttpid = f'trx {Request.destination}' self.bidir = bidir # test that trx_type belongs to eqpt_config.json # if not replace it with a default try: if equipment['Transceiver'][Request.trx_type]: self.trx_type = correct_xlrd_int_to_str_reading( Request.trx_type) if Request.mode is not None: Requestmode = correct_xlrd_int_to_str_reading(Request.mode) if [ mode for mode in equipment['Transceiver'] [Request.trx_type].mode if mode['format'] == Requestmode ]: self.mode = Requestmode else: msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.' # print(msg) logger.critical(msg) raise ServiceError(msg) else: Requestmode = None self.mode = Request.mode except KeyError: msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Request.mode}\' in eqpt library \nComputation stopped.' # print(msg) logger.critical(msg) raise ServiceError(msg) # excel input are in GHz and dBm if Request.spacing is not None: self.spacing = Request.spacing * 1e9 else: msg = f'Request {self.request_id} missing spacing: spacing is mandatory.\ncomputation stopped' logger.critical(msg) raise ServiceError(msg) if Request.power is not None: self.power = db2lin(Request.power) * 1e-3 else: self.power = None if Request.nb_channel is not None: self.nb_channel = int(Request.nb_channel) else: self.nb_channel = None value = correct_xlrd_int_to_str_reading(Request.disjoint_from) self.disjoint_from = [n for n in value.split(' | ') if value] self.nodes_list = [] if Request.nodes_list: self.nodes_list = Request.nodes_list.split(' | ') self.loose = 'LOOSE' if Request.is_loose.lower() == 'no': self.loose = 'STRICT' self.path_bandwidth = None if Request.path_bandwidth is not None: self.path_bandwidth = Request.path_bandwidth * 1e9 else: self.path_bandwidth = 0
def correct_xls_route_list(network_filename, network, pathreqlist): """ prepares the format of route list of nodes to be consistant with nodes names: remove wrong names, find correct names for ila, roadm and fused if the entry was xls. if it was not xls, all names in list should be exact name in the network. """ # first loads the base correspondance dict built with excel naming corresp_roadm, corresp_fused, corresp_ila = corresp_names( network_filename, network) # then correct dict names with names of the autodisign and find next_node name # according to xls naming corresp_ila, next_node = corresp_next_node(network, corresp_ila, corresp_roadm) # finally correct constraints based on these dict trxfibertype = [ n.uid for n in network.nodes() if isinstance(n, (Transceiver, Fiber)) ] roadmtype = [n.uid for n in network.nodes() if isinstance(n, Roadm)] edfatype = [n.uid for n in network.nodes() if isinstance(n, Edfa)] # TODO there is a problem of identification of fibers in case of parallel # fibers between two adjacent roadms so fiber constraint is not supported transponders = [ n.uid for n in network.nodes() if isinstance(n, Transceiver) ] for pathreq in pathreqlist: # first check that source and dest are transceivers if pathreq.source not in transponders: msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find' +\ f' transponder source : {pathreq.source}.{ansi_escapes.reset}' logger.critical(msg) raise ServiceError(msg) if pathreq.destination not in transponders: msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find' +\ f' transponder destination: {pathreq.destination}.{ansi_escapes.reset}' logger.critical(msg) raise ServiceError(msg) # silently pop source and dest nodes from the list if they were added by the user as first # and last elem in the constraints respectively. Other positions must lead to an error # caught later on if pathreq.nodes_list and pathreq.source == pathreq.nodes_list[0]: pathreq.loose_list.pop(0) pathreq.nodes_list.pop(0) if pathreq.nodes_list and pathreq.destination == pathreq.nodes_list[-1]: pathreq.loose_list.pop(-1) pathreq.nodes_list.pop(-1) # Then process user defined constraints with respect to automatic namings temp = deepcopy(pathreq) # This needs a temporary object since we may suppress/correct elements in the list # during the process for i, n_id in enumerate(temp.nodes_list): # n_id must not be a transceiver and must not be a fiber (non supported, user # can not enter fiber names in excel) if n_id not in trxfibertype: # check that n_id is in the node list, if not find a correspondance name if n_id in roadmtype + edfatype: nodes_suggestion = [n_id] else: # checks first roadm, fused, and ila in this order, because ila automatic name # contain roadm names. If it is a fused node, next ila names might be correct # suggestions, especially if following fibers were splitted and ila names # created with the name of the fused node if n_id in corresp_roadm.keys(): nodes_suggestion = corresp_roadm[n_id] elif n_id in corresp_fused.keys(): nodes_suggestion = corresp_fused[n_id] + corresp_ila[ n_id] elif n_id in corresp_ila.keys(): nodes_suggestion = corresp_ila[n_id] else: nodes_suggestion = [] if nodes_suggestion: try: if len(nodes_suggestion) > 1: # if there is more than one suggestion, we need to choose the direction # we rely on the next node provided by the user for this purpose new_n = next( n for n in nodes_suggestion if n in next_node.keys() and next_node[n] in temp.nodes_list[i:] + [pathreq.destination] and next_node[n] not in temp.nodes_list[:i]) else: new_n = nodes_suggestion[0] if new_n != n_id: # warns the user when the correct name is used only in verbose mode, # eg 'a' is a roadm and correct name is 'roadm a' or when there was # too much ambiguity, 'b' is an ila, its name can be: # Edfa0_fiber (a → b)-xx if next node is c or # Edfa0_fiber (c → b)-xx if next node is a msg = f'{ansi_escapes.yellow}Invalid route node specified:' +\ f'\n\t\'{n_id}\', replaced with \'{new_n}\'{ansi_escapes.reset}' logger.info(msg) pathreq.nodes_list[pathreq.nodes_list.index( n_id)] = new_n except StopIteration: # shall not come in this case, unless requested direction does not exist msg = f'{ansi_escapes.yellow}Invalid route specified {n_id}: could' +\ f' not decide on direction, skipped!.\nPlease add a valid' +\ f' direction in constraints (next neighbour node){ansi_escapes.reset}' print(msg) logger.info(msg) pathreq.loose_list.pop(pathreq.nodes_list.index(n_id)) pathreq.nodes_list.remove(n_id) else: if temp.loose_list[i] == 'LOOSE': # if no matching can be found in the network just ignore this constraint # if it is a loose constraint # warns the user that this node is not part of the topology msg = f'{ansi_escapes.yellow}Invalid node specified:\n\t\'{n_id}\'' +\ f', could not use it as constraint, skipped!{ansi_escapes.reset}' print(msg) logger.info(msg) pathreq.loose_list.pop(pathreq.nodes_list.index(n_id)) pathreq.nodes_list.remove(n_id) else: msg = f'{ansi_escapes.red}Could not find node:\n\t\'{n_id}\' in network' +\ f' topology. Strict constraint can not be applied.{ansi_escapes.reset}' logger.critical(msg) raise ServiceError(msg) else: if temp.loose_list[i] == 'LOOSE': print( f'{ansi_escapes.yellow}Invalid route node specified:\n\t\'{n_id}\'' + f' type is not supported as constraint with xls network input,' + f' skipped!{ansi_escapes.reset}') pathreq.loose_list.pop(pathreq.nodes_list.index(n_id)) pathreq.nodes_list.remove(n_id) else: msg = f'{ansi_escapes.red}Invalid route node specified \n\t\'{n_id}\'' +\ f' type is not supported as constraint with xls network input,' +\ f', Strict constraint can not be applied.{ansi_escapes.reset}' logger.critical(msg) raise ServiceError(msg) return pathreqlist
def compute_requests(network, data, equipment): """ Main program calling functions """ # Build the network once using the default power defined in SI in eqpt config # TODO power density: db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by # spacing, f_min and f_max p_db = equipment['SI']['default'].power_dbm p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,\ equipment['SI']['default'].f_max, equipment['SI']['default'].spacing)) build_network(network, equipment, p_db, p_total_db) save_network(ARGS.network_filename, network) oms_list = build_oms_list(network, equipment) try: rqs = requests_from_json(data, equipment) except ServiceError as this_e: print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}') raise this_e # check that request ids are unique. Non unique ids, may # mess the computation: better to stop the computation all_ids = [r.request_id for r in rqs] if len(all_ids) != len(set(all_ids)): for item in list(set(all_ids)): all_ids.remove(item) msg = f'Requests id {all_ids} are not unique' LOGGER.critical(msg) raise ServiceError(msg) try: rqs = correct_route_list(network, rqs) except ServiceError as this_e: print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}') raise this_e #exit(1) # pths = compute_path(network, equipment, rqs) dsjn = disjunctions_from_json(data) print('\x1b[1;34;40m' + f'List of disjunctions' + '\x1b[0m') print(dsjn) # need to warn or correct in case of wrong disjunction form # disjunction must not be repeated with same or different ids dsjn = correct_disjn(dsjn) # Aggregate demands with same exact constraints print('\x1b[1;34;40m' + f'Aggregating similar requests' + '\x1b[0m') rqs, dsjn = requests_aggregation(rqs, dsjn) # TODO export novel set of aggregated demands in a json file print('\x1b[1;34;40m' + 'The following services have been requested:' + '\x1b[0m') print(rqs) print('\x1b[1;34;40m' + f'Computing all paths with constraints' + '\x1b[0m') try: pths = compute_path_dsjctn(network, equipment, rqs, dsjn) except DisjunctionError as this_e: print( f'{ansi_escapes.red}Disjunction error:{ansi_escapes.reset} {this_e}' ) raise this_e print('\x1b[1;34;40m' + f'Propagating on selected path' + '\x1b[0m') propagatedpths, reversed_pths, reversed_propagatedpths = \ compute_path_with_disjunction(network, equipment, rqs, pths) # Note that deepcopy used in compute_path_with_disjunction returns # a list of nodes which are not belonging to network (they are copies of the node objects). # so there can not be propagation on these nodes. pth_assign_spectrum(pths, rqs, oms_list, reversed_pths) print('\x1b[1;34;40m' + f'Result summary' + '\x1b[0m') header = ['req id', ' demand', ' snr@bandwidth A-Z (Z-A)', ' [email protected] A-Z (Z-A)',\ ' Receiver minOSNR', ' mode', ' Gbit/s', ' nb of tsp pairs',\ 'N,M or blocking reason'] data = [] data.append(header) for i, this_p in enumerate(propagatedpths): rev_pth = reversed_propagatedpths[i] if rev_pth and this_p: psnrb = f'{round(mean(this_p[-1].snr),2)} ({round(mean(rev_pth[-1].snr),2)})' psnr = f'{round(mean(this_p[-1].snr_01nm), 2)}' +\ f' ({round(mean(rev_pth[-1].snr_01nm),2)})' elif this_p: psnrb = f'{round(mean(this_p[-1].snr),2)}' psnr = f'{round(mean(this_p[-1].snr_01nm),2)}' try: if rqs[i].blocking_reason in BLOCKING_NOPATH: line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} :',\ f'-', f'-', f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',\ f'-', f'{rqs[i].blocking_reason}'] else: line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,\ psnr, f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9, 2)}',\ f'-', f'{rqs[i].blocking_reason}'] except AttributeError: line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,\ psnr, f'{rqs[i].OSNR}', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',\ f'{ceil(rqs[i].path_bandwidth / rqs[i].bit_rate) }', f'({rqs[i].N},{rqs[i].M})'] data.append(line) col_width = max(len(word) for row in data for word in row[2:]) # padding firstcol_width = max(len(row[0]) for row in data) # padding secondcol_width = max(len(row[1]) for row in data) # padding for row in data: firstcol = ''.join(row[0].ljust(firstcol_width)) secondcol = ''.join(row[1].ljust(secondcol_width)) remainingcols = ''.join( word.center(col_width, ' ') for word in row[2:]) print(f'{firstcol} {secondcol} {remainingcols}') print('\x1b[1;33;40m'+f'Result summary shows mean SNR and OSNR (average over all channels)' +\ '\x1b[0m') return propagatedpths, reversed_propagatedpths, rqs