mirror of
				https://github.com/Telecominfraproject/oopt-gnpy.git
				synced 2025-10-31 10:07:57 +00:00 
			
		
		
		
	 acafc78456
			
		
	
	acafc78456
	
	
	
		
			
			This is inspired by #293. The original issue was that the transponder OSNR was not accounted for. Rather than making the propagate() and propagate2() more complex, let's move the actual path printing to the demo code. There's no secret sauce in there, it's just a simple for-each-print thingy, so let's remove it from the library code. fixes #262
		
			
				
	
	
		
			924 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			924 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/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 sys import exit
 | |
| from collections import namedtuple
 | |
| from logging import getLogger, basicConfig, CRITICAL, DEBUG, INFO
 | |
| from networkx import (dijkstra_path, NetworkXNoPath, all_simple_paths,shortest_path_length)
 | |
| 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 copy import copy, deepcopy
 | |
| from csv import writer
 | |
| from math import ceil
 | |
| 
 | |
| logger = getLogger(__name__)
 | |
| 
 | |
| 
 | |
| RequestParams = namedtuple('RequestParams','request_id source destination 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:
 | |
|     def __init__(self, *args, **params):
 | |
|         params = RequestParams(**params)
 | |
|         self.request_id = params.request_id
 | |
|         self.source     = params.source
 | |
|         self.destination = params.destination
 | |
|         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:
 | |
|     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'])
 | |
| 
 | |
| class Result_element(Element):
 | |
|     def __init__(self,path_request,computed_path):
 | |
|         self.path_id = path_request.request_id
 | |
|         self.path_request = path_request
 | |
|         self.computed_path = computed_path
 | |
|         hop_type = []
 | |
|         if len(computed_path)>0 :
 | |
|             for e in computed_path :
 | |
|                 if isinstance(e, Transceiver) :
 | |
|                     hop_type.append(' - '.join([path_request.tsp,path_request.tsp_mode]))
 | |
|                 else:
 | |
|                     hop_type.append('not recorded')
 | |
|         else:
 | |
|             # TODO differentiate empty path in case not feasible because of tsp or not feasible because
 | |
|             # ther is no path connecting the nodes (whatever the tsp)
 | |
|             mode = 'not feasible with this transponder'
 | |
|             hop_type = ' - '.join([path_request.tsp,mode])
 | |
|         self.hop_type = hop_type
 | |
|     uid = property(lambda self: repr(self))
 | |
|     @property
 | |
|     def pathresult(self):
 | |
|         if not self.computed_path:
 | |
|             return {
 | |
|                    'path-id': self.path_id,
 | |
|                    'path-properties':{
 | |
|                        'path-metric': [
 | |
|                            {
 | |
|                            'metric-type': 'SNR@bandwidth',
 | |
|                            'accumulative-value': 'None'
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'SNR@0.1nm',
 | |
|                            'accumulative-value': 'None'
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'OSNR@bandwidth',
 | |
|                            'accumulative-value': 'None'
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'OSNR@0.1nm',
 | |
|                            'accumulative-value': 'None'
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'reference_power',
 | |
|                            'accumulative-value': self.path_request.power
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'path_bandwidth',
 | |
|                            'accumulative-value': self.path_request.path_bandwidth
 | |
|                            }
 | |
|                         ],
 | |
|                         'path-srlgs': {
 | |
|                             'usage': 'not used yet',
 | |
|                             'values': 'not used yet'
 | |
|                         },
 | |
|                         'path-route-objects': [
 | |
|                             {
 | |
|                             'path-route-object': {
 | |
|                                 'index': 0,
 | |
|                                 'unnumbered-hop': {
 | |
|                                     'node-id': self.path_request.source,
 | |
|                                     'link-tp-id': self.path_request.source,
 | |
|                                     'hop-type':  self.hop_type,
 | |
|                                     'direction': 'not used'
 | |
|                                 },
 | |
|                                 'label-hop': {
 | |
|                                     'te-label': {
 | |
|                                         'generic': 'not used yet',
 | |
|                                         'direction': 'not used yet'
 | |
|                                         }
 | |
|                                     }
 | |
|                                 }
 | |
|                             },
 | |
|                             {
 | |
|                             'path-route-object': {
 | |
|                                 'index': 1,
 | |
|                                 'unnumbered-hop': {
 | |
|                                     'node-id': self.path_request.destination,
 | |
|                                     'link-tp-id': self.path_request.destination,
 | |
|                                     'hop-type':  self.hop_type,
 | |
|                                     'direction': 'not used'
 | |
|                                 },
 | |
|                                 'label-hop': {
 | |
|                                     'te-label': {
 | |
|                                         'generic': 'not used yet',
 | |
|                                         'direction': 'not used yet'
 | |
|                                         }
 | |
|                                     }
 | |
|                                 }
 | |
|                             }
 | |
|                             ]
 | |
|                     }
 | |
|                 }
 | |
|         else:
 | |
|             return {
 | |
|                    'path-id': self.path_id,
 | |
|                    'path-properties':{
 | |
|                        'path-metric': [
 | |
|                            {
 | |
|                            'metric-type': 'SNR@bandwidth',
 | |
|                            'accumulative-value': round(mean(self.computed_path[-1].snr),2)
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'SNR@0.1nm',
 | |
|                            'accumulative-value': round(mean(self.computed_path[-1].snr+lin2db(self.path_request.baud_rate/12.5e9)),2)
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'OSNR@bandwidth',
 | |
|                            'accumulative-value': round(mean(self.computed_path[-1].osnr_ase),2)
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'OSNR@0.1nm',
 | |
|                            'accumulative-value': round(mean(self.computed_path[-1].osnr_ase_01nm),2)
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'reference_power',
 | |
|                            'accumulative-value': self.path_request.power
 | |
|                            },
 | |
|                            {
 | |
|                            'metric-type': 'path_bandwidth',
 | |
|                            'accumulative-value': self.path_request.path_bandwidth
 | |
|                            }
 | |
|                         ],
 | |
|                         'path-srlgs': {
 | |
|                             'usage': 'not used yet',
 | |
|                             'values': 'not used yet'
 | |
|                         },
 | |
|                         'path-route-objects': [
 | |
|                             {
 | |
|                             'path-route-object': {
 | |
|                                 'index': self.computed_path.index(n),
 | |
|                                 'unnumbered-hop': {
 | |
|                                     'node-id': n.uid,
 | |
|                                     'link-tp-id': n.uid,
 | |
|                                     'hop-type': self.hop_type[self.computed_path.index(n)],
 | |
|                                     'direction': 'not used'
 | |
|                                 },
 | |
|                                 'label-hop': {
 | |
|                                     'te-label': {
 | |
|                                         'generic': 'not used yet',
 | |
|                                         'direction': 'not used yet'
 | |
|                                         }
 | |
|                                     }
 | |
|                                 }
 | |
|                             } for n in self.computed_path
 | |
|                             ]
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|     @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 in req.nodes_list :
 | |
|         # for debug excel print(n)
 | |
|         nodes_list.append(next(el for el in anytypenode if el.uid == n))
 | |
|     # nodes_list contains at least the destination
 | |
|     if nodes_list is None :
 | |
|         msg = f'Request {req.request_id} problem in the constitution of nodes_list: should at least include destination'
 | |
|         logger.critical(msg)
 | |
|         exit()
 | |
|     if req.nodes_list[-1] != req.destination:
 | |
|         msg = f'Request {req.request_id} malformed list of nodes: last node should be destination trx'
 | |
|         logger.critical(msg)
 | |
|         exit()
 | |
| 
 | |
|     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 {source.uid} to node : {destination.uid} in network topology'+ '\x1b[0m'
 | |
|             logger.critical(msg)
 | |
|             print(msg)
 | |
|             total_path = []        
 | |
|     else : 
 | |
|         all_simp_pths = list(all_simple_paths(network,source=source,\
 | |
|             target=destination, cutoff=120))
 | |
|         candidate = []
 | |
|         for p in all_simp_pths :
 | |
|             if ispart(nodes_list, p) :
 | |
|                 # print(f'selection{[el.uid for el in p if el in roadm]}')
 | |
|                 candidate.append(p)
 | |
|         # 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:
 | |
|             if req.loose_list[req.nodes_list.index(n)] == 'loose':
 | |
|                 print(f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path crossing {nodes_list} in network topology'+ '\x1b[0m')
 | |
|                 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 crossing {nodes_list}.\nNo path computed'+ '\x1b[0m'
 | |
|                 logger.critical(msg)
 | |
|                 print(msg)
 | |
|                 total_path = []
 | |
| 
 | |
|     # obsolete method: 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 segmenst 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
 | |
|     #
 | |
|     # 
 | |
|     # total_path = [source]
 | |
| 
 | |
|     # for n in req.nodes_list:
 | |
|     #     try :
 | |
|     #         node = next(el for el in trx if el.uid == n)
 | |
|     #     except StopIteration:
 | |
|     #         try:
 | |
|     #             node = next(el for el in anytypenode if el.uid == n)
 | |
|     #         except StopIteration:
 | |
|     #             try:
 | |
|     #                 # TODO this test is not giving good results: full name of the 
 | |
|     #                 # amp is required to avoid ambiguity on the direction
 | |
|     #                 node = next(el for el in anytypenode 
 | |
|     #                     if n in el.uid)
 | |
|     #             except StopIteration:
 | |
|     #                 msg = f'could not find node : {n} in network topology: \
 | |
|     #                     not a trx, roadm, edfa, fiber or fused element'
 | |
|     #                 logger.critical(msg)
 | |
|     #                 raise ValueError(msg)
 | |
|     #     # extend path list without repeating source -> skip first element in the list
 | |
|     #     try:
 | |
|     #         # to avoid looping back: use an alternate graph were current path edges and vertex are suppressed
 | |
| 
 | |
|     #         total_path.extend(dijkstra_path(network, source, node)[1:])
 | |
|     #         source = node
 | |
|     #     except NetworkXNoPath:
 | |
|     #         if req.loose_list[req.nodes_list.index(n)] == 'loose':
 | |
|     #             print(f'could not find a path from {source.uid} to loose node : {n} in network topology')
 | |
|     #             print(f'node  {n} is skipped')
 | |
|     #         else:
 | |
|     #             msg = f'could not find a path from {source.uid} to node : {n} in network topology'
 | |
|     #             logger.critical(msg)
 | |
|     #             print(msg)
 | |
|     #             total_path = []
 | |
| 
 | |
|     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 el in path:
 | |
|         si = el(si)
 | |
|     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 el in path:
 | |
|         before_si = si
 | |
|         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([m['baud_rate'] for m in equipment['Transceiver'][req.tsp].mode 
 | |
|         if float(m['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 b in baudrate_to_explore :
 | |
|             modes_to_explore = [m for m in equipment['Transceiver'][req.tsp].mode 
 | |
|                 if m['baud_rate'] == b and float(m['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
 | |
|             si = create_input_spectral_information(
 | |
|             req.f_min, req.f_max, equipment['SI']['default'].roll_off,
 | |
|             b, req.power, req.spacing)
 | |
|             for el in path:
 | |
|                 si = el(si)
 | |
|             for m in modes_to_explore :
 | |
|                 if path[-1].snr is not None:
 | |
|                     path[-1].update_snr(m['tx_osnr'], equipment['Roadm']['default'].add_drop_osnr)
 | |
|                     if round(min(path[-1].snr+lin2db(b/(12.5e9))),2) > m['OSNR'] :
 | |
|                         found_a_feasible_mode = True
 | |
|                         return path, m
 | |
|                 else:  
 | |
|                     return [], 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)
 | |
|         return [],None
 | |
|     else :
 | |
|     #  no baudrate satisfying spacing
 | |
|         msg = f'\tWarning! Request {req.request_id}: no baudrate satisfies spacing requirement.\n'
 | |
|         print(msg)
 | |
|         logger.info(msg)
 | |
|         return [], None
 | |
| 
 | |
| 
 | |
| def jsontocsv(json_data,equipment,fileout):
 | |
|     # read 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(('path-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'))
 | |
|     tspjsondata = equipment['Transceiver']
 | |
|     #print(tspjsondata)
 | |
|     for p in json_data['path']:
 | |
|         path_id     = p['path-id']
 | |
|         source      = p['path-properties']['path-route-objects'][0]\
 | |
|         ['path-route-object']['unnumbered-hop']['node-id']
 | |
|         destination = p['path-properties']['path-route-objects'][-1]\
 | |
|         ['path-route-object']['unnumbered-hop']['node-id']
 | |
|         # selects only roadm nodes
 | |
|         pth        = ' | '.join([ e['path-route-object']['unnumbered-hop']['node-id']
 | |
|                  for e in p['path-properties']['path-route-objects'] 
 | |
|                  if e['path-route-object']['unnumbered-hop']['node-id'].startswith('roadm') or e['path-route-object']['unnumbered-hop']['node-id'].startswith('Edfa')])
 | |
| 
 | |
|         [tsp,mode] = p['path-properties']['path-route-objects'][0]\
 | |
|         ['path-route-object']['unnumbered-hop']['hop-type'].split(' - ')
 | |
| 
 | |
|         # find the min  acceptable OSNR, baud rate from the eqpt library based on tsp (tupe) and mode (format)
 | |
|         # loading equipment already tests the existence of tsp type and mode:
 | |
|         if mode !='not feasible with this transponder' :
 | |
|             [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] = ['','','','']
 | |
|         output_snr = next(e['accumulative-value'] 
 | |
|             for e in p['path-properties']['path-metric'] if e['metric-type'] == 'SNR@0.1nm')
 | |
|         output_snrbandwidth = next(e['accumulative-value']
 | |
|             for e in p['path-properties']['path-metric'] if e['metric-type'] == 'SNR@bandwidth')
 | |
|         output_osnr = next(e['accumulative-value']
 | |
|             for e in p['path-properties']['path-metric'] if e['metric-type'] == 'OSNR@0.1nm')
 | |
|         output_osnrbandwidth = next(e['accumulative-value']
 | |
|             for e in p['path-properties']['path-metric'] if e['metric-type'] == 'OSNR@bandwidth')
 | |
|         power = next(e['accumulative-value']
 | |
|             for e in p['path-properties']['path-metric'] if e['metric-type'] == 'reference_power')
 | |
|         path_bandwidth = next(e['accumulative-value']
 | |
|             for e in p['path-properties']['path-metric'] if e['metric-type'] == 'path_bandwidth')
 | |
|         if isinstance(output_snr, str):
 | |
|             isok = False
 | |
|             nb_tsp = 0
 | |
|             pthbdbw = round(path_bandwidth*1e-9,2)
 | |
|             rosnr = ''
 | |
|             rsnr = ''
 | |
|             rsnrb = ''
 | |
|             br = ''
 | |
|             pw = ''
 | |
|             total_cost = ''
 | |
|         else:
 | |
|             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)
 | |
|             br     = round(baud_rate*1e-9,2) 
 | |
|             pw     = round(lin2db(power)+30,2)
 | |
|             total_cost = nb_tsp * cost
 | |
|         mywriter.writerow((path_id,
 | |
|             source,
 | |
|             destination,
 | |
|             pthbdbw,
 | |
|             isok,
 | |
|             nb_tsp,
 | |
|             total_cost,
 | |
|             tsp,
 | |
|             mode,
 | |
|             rosnr,
 | |
|             rsnr,
 | |
|             rsnrb,
 | |
|             br,
 | |
|             pw,
 | |
|             pth
 | |
|             ))
 | |
| 
 | |
| 
 | |
| 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      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,network))
 | |
|         rqs[pathreq.request_id] = all_simp_pths 
 | |
|         temp =[]
 | |
|         for p 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
 | |
|             s = [e.uid for i,e in enumerate(p[1:-1]) \
 | |
|                 if (isinstance(e,Roadm) | (isinstance(p[i],Roadm) ))] 
 | |
|             temp.append(s)
 | |
|             # id(s) is unique even if path is the same: two objects with same 
 | |
|             # path have two different ids
 | |
|             allpaths[id(s)] = Pth(pathreq,p,s)
 | |
|         simple_rqs[pathreq.request_id] = temp
 | |
|         temp =[]
 | |
|         for p 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(p[1:-1]) \
 | |
|                 if (isinstance(e,Roadm) | (isinstance(p[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,p in enumerate(simple_rqs[dlist[0]]):
 | |
|             dpath.append([p])
 | |
|             # 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 e1 in dlist[1:] :
 | |
|             temp = []
 | |
|             for j,p1 in enumerate(simple_rqs[e1]):
 | |
|                 # allpaths[id(p1)].d_id = d.disjunction_id
 | |
|                 # can use index j in simple_rqs_reversed because index 
 | |
|                 # of direct and reversed paths have been kept identical
 | |
|                 p1_reversed = simple_rqs_reversed[e1][j]
 | |
|                 # print(p1_reversed)
 | |
|                 # print('\n\n')
 | |
|                 for k,c in enumerate(dpath) :
 | |
|                     # print(f' c: \t{c}')
 | |
|                     temp2 = c.copy()
 | |
|                     all_disjoint = 0
 | |
|                     for p in c :
 | |
|                         all_disjoint += isdisjoint(p1,p)+ isdisjoint(p1_reversed,p)
 | |
|                     if all_disjoint ==0:
 | |
|                         temp2.append(p1)
 | |
|                         temp.append(temp2)
 | |
|                             # print(f' coucou {e1}: \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 p in candidate_paths :
 | |
|             iscandidate = 0
 | |
|             for sol in concerned_d_id :
 | |
|                 test = 1
 | |
|                 # for each solution test if p is part of the solution
 | |
|                 # if yes, then p can remain a candidate
 | |
|                 for i,m in enumerate(candidates[sol]) :
 | |
|                     if p in m:
 | |
|                         if allpaths[id(m[m.index(p)])].req.request_id == pathreq.request_id :
 | |
|                             test = 0
 | |
|                             break
 | |
|                 iscandidate += test
 | |
|             if iscandidate != 0:
 | |
|                 for l in concerned_d_id :
 | |
|                     for m in candidates[l] :
 | |
|                         if p in m :
 | |
|                             candidates[l].remove(m)
 | |
| 
 | |
| #    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 d in disjunctions_list  :
 | |
|         temp = []
 | |
|         for j,sol in enumerate(candidates[d.disjunction_id]) :
 | |
|             testispartok = True
 | |
|             for i,p in enumerate(sol) :
 | |
|                 # print(f'test {allpaths[id(p)].req.request_id}')
 | |
|                 # print(f'length of route {len(allpaths[id(p)].req.nodes_list)}')
 | |
|                 if allpaths[id(p)].req.nodes_list :
 | |
|                     # if p 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(p)].req.nodes_list, p) : 
 | |
|                         # print(f'nb of solutions {len(temp)}')
 | |
|                         if j < len(candidates[d.disjunction_id])-1 :
 | |
|                             msg = f'removing {sol}'
 | |
|                             logger.info(msg)
 | |
|                             testispartok = False
 | |
|                             #break
 | |
|                         else:
 | |
|                             if 'loose' in allpaths[id(p)].req.loose_list:
 | |
|                                 logger.info(f'Could not apply route constraint'+
 | |
|                                     f'{allpaths[id(p)].req.nodes_list} on request {allpaths[id(p)].req.request_id}')
 | |
|                             else :
 | |
|                                 logger.info(f'removing last solution from candidate paths\n{sol}')
 | |
|                                 testispartok = False
 | |
|             if testispartok :
 | |
|                 temp.append(sol)
 | |
|         candidates[d.disjunction_id] = temp
 | |
| 
 | |
|     # step 5 select the first combination that works
 | |
|     pathreslist_disjoint = {}
 | |
|     for d in disjunctions_list  :
 | |
|         test_sol = True
 | |
|         while test_sol:
 | |
|             # print('coucou')
 | |
|             if candidates[d.disjunction_id] :
 | |
|                 for p in candidates[d.disjunction_id][0]:
 | |
|                     if allpaths[id(p)].req in pathreqlist_disjt: 
 | |
|                         # print(f'selected path :{p} for req {allpaths[id(p)].req.request_id}')
 | |
|                         pathreslist_disjoint[allpaths[id(p)].req] = allpaths[id(p)].pth
 | |
|                         pathreqlist_disjt.remove(allpaths[id(p)].req)
 | |
|                         candidates = remove_candidate(candidates, allpaths, allpaths[id(p)].req, p)
 | |
|                         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
 | |
|                 exit()
 | |
|     
 | |
|     # 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(p1,p2) :
 | |
|     # returns 0 if disjoint
 | |
|     edge1 = list(pairwise(p1))
 | |
|     edge2 = list(pairwise(p2))
 | |
|     for e in edge1 :
 | |
|         if e in edge2 :
 | |
|             return 1
 | |
|     return 0
 | |
| 
 | |
| def find_reversed_path(p,network) :
 | |
|     # 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
 | |
|     reversed_roadm_path = list(reversed([e for e in p if isinstance (e,Roadm)]))
 | |
|     source = p[-1]
 | |
|     destination = p[0]
 | |
|     total_path = [source]
 | |
|     for node in reversed_roadm_path :
 | |
|         total_path.extend(dijkstra_path(network, source, node, weight = 'weight')[1:])
 | |
|         source = node
 | |
|     total_path.append(destination)
 | |
|     return total_path
 | |
| 
 | |
| def ispart(a,b) :
 | |
|     # 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 i, el in enumerate(a):
 | |
|         if el in b :
 | |
|             if b.index(el) >= j :
 | |
|                 j = b.index(el)
 | |
|             else: 
 | |
|                 return False
 | |
|         else:
 | |
|             return False
 | |
|     return True
 | |
| 
 | |
| def remove_candidate(candidates, allpaths, rq, pth) :
 | |
|     # print(f'coucou {rq.request_id}')
 | |
|     for key, candidate  in candidates.items() :
 | |
|         temp = candidate.copy()
 | |
|         for i,sol in enumerate(candidate) :
 | |
|             for p in sol :
 | |
|                 if allpaths[id(p)].req.request_id == rq.request_id :
 | |
|                     if id(p) != id(pth) :
 | |
|                         temp.remove(sol)
 | |
|                         break
 | |
|         candidates[key] = temp
 | |
|     return candidates
 | |
| 
 | |
| def compare_reqs(req1,req2,disjlist) :
 | |
|     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 d in dis1:
 | |
|             temp1.extend(d.disjunctions_req)
 | |
|             temp1.remove(req1.request_id)
 | |
|         temp2 = []
 | |
|         for d in dis2:
 | |
|             temp2.extend(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 r in local_list : 
 | |
|             if  req.request_id != r.request_id and compare_reqs(req, r, disjlist):
 | |
|                 # aggregate
 | |
|                 r.path_bandwidth += req.path_bandwidth
 | |
|                 temp_r_id = r.request_id
 | |
|                 r.request_id = ' | '.join((r.request_id,req.request_id))
 | |
|                 # remove request from list
 | |
|                 local_list.remove(req)
 | |
|                 # todo change also disjunction req with new demand
 | |
| 
 | |
|                 for d in disjlist :
 | |
|                     if req.request_id in d.disjunctions_req :
 | |
|                         d.disjunctions_req.remove(req.request_id)
 | |
|                         d.disjunctions_req.append(r.request_id)
 | |
|                 for d in disjlist :        
 | |
|                     if temp_r_id in d.disjunctions_req :
 | |
|                         disjlist.remove(d)
 | |
|                 break
 | |
|     return local_list, disjlist
 |