Files
oopt-gnpy/gnpy/core/request.py
2019-10-10 09:02:19 +01:00

1047 lines
46 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 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'
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))
@property
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
@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
}
else:
path_properties = {
'path-metric': path_metric(self.computed_path, self.path_request),
'path-route-objects': self.detailed_path_json
}
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 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([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 el in path:
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