Files
oopt-gnpy/gnpy/core/request.py
EstherLerouzic 39a8fa3335 Adding aggregation feature
- add bitrate in the json service file and enable to support both with and without bitrate
- change mode to optional
- add an aggregation function that groups identical demands (except for id and bitrate)
  TODO : check which mode can satisfy the request or how many transponders  -> going towards
  dimensionning
- Correcting some bug on loose parameter: hop-type attribute was forced to be always 'loose'
  -> now reads the excel entry
- service sheets checks on header if field names are corrects, but any order is
  now feasible.

Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2018-11-21 09:06:32 +00:00

698 lines
30 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)
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.network import set_roadm_loss
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
logger = getLogger(__name__)
RequestParams = namedtuple('RequestParams','request_id source destination trx_type'+
' trx_mode nodes_list loose_list spacing power nb_channel frequency format baud_rate OSNR bit_rate roll_off')
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.frequency = params.frequency
self.format = params.format
self.OSNR = params.OSNR
self.bit_rate = params.bit_rate
self.roll_off = params.roll_off
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):
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{self.baud_rate * 1e-9} Gbaud',
f'bit_rate:\t{self.bit_rate * 1e-9} 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'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'relaxable: {self.relaxable}',
f'link-diverse: {self.link_diverse}',
f'node-diverse: {self.node_diverse}',
f'request-id-numbers: {self.disjunctions_req}']
)
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 = []
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')
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
}
],
'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': ' - '.join([self.path_request.tsp, self.path_request.tsp_mode]),
'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': ' - '.join([self.path_request.tsp, self.path_request.tsp_mode]),
'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
}
],
'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)
# start the path with its source
# TODO : avoid loops due to constraints , guess name based on string,
# avoid crashing if on req is not correct
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 el.uid.find(f'{n}'))
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:
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, show=False):
#update roadm loss in case of power sweep (power mode only)
set_roadm_loss(path, equipment, lin2db(req.power*1e3))
si = create_input_spectral_information(
req.frequency['min'], req.roll_off,
req.baud_rate, req.power, req.spacing, req.nb_channel)
for el in path:
si = el(si)
if show :
print(el)
return path
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','transponder-type',\
'transponder-mode','baud rate (Gbaud)', 'input power (dBm)','path',\
'OSNR@bandwidth','OSNR@0.1nm','SNR@bandwidth','SNR@0.1nm','Pass?'))
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']
pth = ' | '.join([ e['path-route-object']['unnumbered-hop']['node-id']
for e in p['path-properties']['path-route-objects']])
[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:
[minosnr, baud_rate] = next([m['OSNR'] , m['baud_rate']]
for m in equipment['Transceiver'][tsp].mode if m['format']==mode)
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')
if isinstance(output_snr, str):
isok = ''
else:
isok = output_snr >= minosnr
mywriter.writerow((path_id,
source,
destination,
tsp,
mode,
baud_rate*1e-9,
round(lin2db(power)+30,2),
pth,
output_osnrbandwidth,
output_osnr,
output_snrbandwidth,
output_snr,
isok
))
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)))
# sort them
all_simp_pths = sorted(all_simp_pths, key=lambda path: len(path))
# 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)[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) :
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.frequency == req2.frequency and \
req1.format == req2.format and \
req1.OSNR == req2.OSNR and \
req1.roll_off == req2.roll_off :
print(f'coucou {req1.request_id} {req2.request_id}')
print(f'{req1.nodes_list} == {req2.nodes_list}')
return True
else:
return False
def requests_aggregation(pathreqlist) :
# 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 local_list:
for r in local_list :
if req.request_id != r.request_id and compare_reqs(req, r):
# aggregate
req.bit_rate += r.bit_rate
req.request_id = ' | '.join((req.request_id,r.request_id))
# remove request from list
local_list.remove(r)
return local_list