mirror of
https://github.com/Telecominfraproject/oopt-gnpy.git
synced 2025-10-30 17:47:50 +00:00
This will be used to compute the max total power for design per OMS. Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com> Change-Id: I392f06c792af9f32d4a14324c683bd3fae655de8
1310 lines
61 KiB
Python
1310 lines
61 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
# gnpy.topology.request: path computation functionality
|
|
# Copyright (C) 2025 Telecom Infra Project and GNPy contributors
|
|
# see AUTHORS.rst for a list of contributors
|
|
|
|
"""
|
|
gnpy.topology.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 typing import List
|
|
from logging import getLogger
|
|
from networkx import (dijkstra_path, NetworkXNoPath,
|
|
all_simple_paths, shortest_simple_paths)
|
|
from networkx.utils import pairwise
|
|
from numpy import mean, argmin
|
|
|
|
from gnpy.core.elements import Transceiver, Roadm, Edfa, Multiband_amplifier
|
|
from gnpy.core.utils import lin2db, unique_ordered, find_common_range
|
|
from gnpy.core.info import create_input_spectral_information, carriers_to_spectral_information, \
|
|
demuxed_spectral_information, muxed_spectral_information, SpectralInformation
|
|
from gnpy.core import network as network_module
|
|
from gnpy.core.exceptions import ServiceError, DisjunctionError
|
|
from copy import 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 penalties bit_rate'
|
|
' roll_off tx_osnr min_spacing cost path_bandwidth effective_freq_slot'
|
|
' equalization_offset_db, tx_power')
|
|
DisjunctionParams = namedtuple('DisjunctionParams', 'disjunction_id relaxable link_diverse'
|
|
' node_diverse disjunctions_req')
|
|
|
|
|
|
class PathRequest:
|
|
"""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.penalties = params.penalties
|
|
self.bit_rate = params.bit_rate
|
|
self.roll_off = params.roll_off
|
|
self.tx_osnr = params.tx_osnr
|
|
self.tx_power = params.tx_power
|
|
self.min_spacing = params.min_spacing
|
|
self.cost = params.cost
|
|
self.path_bandwidth = params.path_bandwidth
|
|
if params.effective_freq_slot is not None:
|
|
self.N = [s['N'] for s in params.effective_freq_slot]
|
|
self.M = [s['M'] for s in params.effective_freq_slot]
|
|
self.initial_spectrum = None
|
|
self.offset_db = params.equalization_offset_db
|
|
|
|
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 and self.bit_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'tx_power_dbm: \t{round(lin2db(self.tx_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', 'NOT_ENOUGH_RESERVED_SPECTRUM']
|
|
|
|
|
|
class ResultElement:
|
|
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 not hasattr(self.path_request, 'blocking_reason'):
|
|
# M and N values should not be None at this point
|
|
if self.path_request.M is None or self.path_request.N is None:
|
|
raise ServiceError('request {self.path_id} should have positive non null n and m values.')
|
|
|
|
temp = {
|
|
'path-route-object': {
|
|
'index': index,
|
|
"label-hop": [{
|
|
"N": n,
|
|
"M": m
|
|
} for n, m in zip(self.path_request.N, self.path_request.M)],
|
|
}
|
|
}
|
|
pro_list.append(temp)
|
|
index += 1
|
|
else:
|
|
# if the path is blocked, no label object is created, but
|
|
# the json response includes a detailed path for user information.
|
|
# M and N values should be None at this point
|
|
if self.path_request.M is not None or self.path_request.N is not None:
|
|
raise ServiceError('request {self.path_id} should not have label M and N values at this point.')
|
|
|
|
|
|
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):
|
|
# nodes_list contains at least the destination
|
|
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')
|
|
raise ValueError()
|
|
|
|
trx = [n for n in network if isinstance(n, Transceiver)]
|
|
source = next(el for el in trx if el.uid == req.source)
|
|
destination = next(el for el in trx if el.uid == req.destination)
|
|
|
|
nodes_list = []
|
|
for node in req.nodes_list[:-1]:
|
|
nodes_list.append(next(el for el in network if el.uid == node))
|
|
total_path = explicit_path(nodes_list, source, destination, network)
|
|
if total_path is not None:
|
|
return total_path
|
|
try:
|
|
path_generator = shortest_simple_paths(network, source, destination, weight='weight')
|
|
total_path = next(path for path in path_generator if ispart(nodes_list, path))
|
|
except NetworkXNoPath:
|
|
msg = (f'Request {req.request_id} could not find a path from'
|
|
f' {source.uid} to node: {destination.uid} in network topology')
|
|
LOGGER.critical(msg)
|
|
req.blocking_reason = 'NO_PATH'
|
|
total_path = []
|
|
except StopIteration:
|
|
# 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
|
|
LOGGER.warning(f'Request {req.request_id} could not find a path crossing '
|
|
f'{[el.uid for el in nodes_list[:-1]]} in network topology')
|
|
|
|
if 'STRICT' not in req.loose_list[:-1]:
|
|
msg = (f'Request {req.request_id} could not find a path with user_'
|
|
f'include node constraints. Constraint ignored')
|
|
LOGGER.warning(msg)
|
|
total_path = dijkstra_path(network, source, destination, weight='weight')
|
|
else:
|
|
# one STRICT makes the whole list STRICT
|
|
msg = (f'Request {req.request_id} could not find a path with user '
|
|
f'include node constraints.\nNo path computed')
|
|
LOGGER.critical(msg)
|
|
req.blocking_reason = 'NO_PATH_WITH_CONSTRAINT'
|
|
total_path = []
|
|
return total_path
|
|
|
|
|
|
def filter_si(path: list, equipment: dict, si: SpectralInformation) -> SpectralInformation:
|
|
"""Filter spectral information based on the amplifiers common range"""
|
|
# First retrieve f_min, f_max spectrum according to amplifiers' spectrum on the path
|
|
common_range = find_elements_common_range(path, equipment)
|
|
# filter out frequencies that should not be created
|
|
filtered_si = []
|
|
for band in common_range:
|
|
temp = demuxed_spectral_information(si, band)
|
|
if temp:
|
|
filtered_si.append(temp)
|
|
if not filtered_si:
|
|
raise ValueError('Defined propagation band does not match amplifiers band.')
|
|
return muxed_spectral_information(filtered_si)
|
|
|
|
|
|
def propagate(path, req, equipment):
|
|
"""propagates signals in each element according to initial spectrum set by user
|
|
Spectrum is specified in request through f_min, f_max and spacing, or initial_spectrum
|
|
and amps frequency band on the path is used to filter out frequencies"""
|
|
# generates spectrum based on request
|
|
if req.initial_spectrum is not None:
|
|
si = carriers_to_spectral_information(initial_spectrum=req.initial_spectrum, power=req.power)
|
|
else:
|
|
si = create_input_spectral_information(
|
|
f_min=req.f_min, f_max=req.f_max, roll_off=req.roll_off, baud_rate=req.baud_rate,
|
|
spacing=req.spacing, tx_osnr=req.tx_osnr, tx_power=req.tx_power, delta_pdb=req.offset_db)
|
|
# filter out frequencies that should not be created
|
|
si = filter_si(path, equipment, si)
|
|
roadm_osnr = []
|
|
for i, el in enumerate(path):
|
|
if isinstance(el, Roadm):
|
|
si = el(si, degree=path[i + 1].uid, from_degree=path[i - 1].uid)
|
|
roadm_osnr.append(el.get_impairment('roadm-osnr', si.frequency,
|
|
from_degree=path[i - 1].uid, degree=path[i + 1].uid))
|
|
else:
|
|
si = el(si)
|
|
path[0].update_snr(si.tx_osnr)
|
|
path[0].calc_penalties(req.penalties)
|
|
roadm_osnr.append(si.tx_osnr)
|
|
path[-1].update_snr(*roadm_osnr)
|
|
path[-1].calc_penalties(req.penalties)
|
|
return si
|
|
|
|
|
|
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 and power offset
|
|
# order higher baudrate with higher power offset first
|
|
baudrate_offset_to_explore = list(set([(this_mode['baud_rate'], this_mode['equalization_offset_db'])
|
|
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_offset_to_explore = sorted(baudrate_offset_to_explore, reverse=True)
|
|
if baudrate_offset_to_explore:
|
|
# at least 1 baudrate can be tested wrt spacing
|
|
for (this_br, this_offset) in baudrate_offset_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'], x['equalization_offset_db']), reverse=True)
|
|
# step2: computes propagation for each baudrate: stop and select the first that passes
|
|
# TODO: the case of roll off is not included: for now use SI one
|
|
# TODO: if the loop in mode optimization does not have a feasible path, then bugs
|
|
if req.initial_spectrum is not None:
|
|
# this case is not yet handled: spectrum can not be defined for the path-request-run function
|
|
# and this function is only called in this case. so coming here should not be considered yet.
|
|
msg = f'Request: {req.request_id} contains a unexpected initial_spectrum.'
|
|
raise ServiceError(msg)
|
|
spc_info = create_input_spectral_information(f_min=req.f_min, f_max=req.f_max,
|
|
roll_off=equipment['SI']['default'].roll_off,
|
|
baud_rate=this_br, spacing=req.spacing,
|
|
delta_pdb=this_offset, tx_osnr=req.tx_osnr,
|
|
tx_power=req.tx_power)
|
|
spc_info = filter_si(path, equipment, spc_info)
|
|
roadm_osnr = []
|
|
for i, el in enumerate(path):
|
|
if isinstance(el, Roadm):
|
|
spc_info = el(spc_info, degree=path[i + 1].uid, from_degree=path[i - 1].uid)
|
|
roadm_osnr.append(el.get_impairment('roadm-osnr', spc_info.frequency,
|
|
from_degree=path[i - 1].uid, degree=path[i + 1].uid))
|
|
else:
|
|
spc_info = el(spc_info)
|
|
for this_mode in modes_to_explore:
|
|
if path[-1].snr is not None:
|
|
path[0].update_snr(this_mode['tx_osnr'])
|
|
path[0].calc_penalties(this_mode['penalties'])
|
|
roadm_osnr.append(this_mode['tx_osnr'])
|
|
path[-1].update_snr(*roadm_osnr)
|
|
# remove the tx_osnr from roadm_osnr list for the next iteration
|
|
del roadm_osnr[-1]
|
|
path[-1].calc_penalties(this_mode['penalties'])
|
|
if round(min(path[-1].snr_01nm - path[-1].total_penalty), 2) \
|
|
> this_mode['OSNR'] + equipment['SI']['default'].sys_margins:
|
|
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'
|
|
LOGGER.warning(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'
|
|
LOGGER.warning(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'{[e["N"] for e in elem["path-route-object"]["label-hop"]]}, '
|
|
+ f'{[e["M"] for e in elem["path-route-object"]["label-hop"]]}')
|
|
# 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 PathRequest 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 dis in disjunctions_list:
|
|
dlist = dis.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])
|
|
# 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: aefhg
|
|
# p2: abfhg
|
|
# 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 p6 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
|
|
candidates[dis.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 = []
|
|
alternatetemp = []
|
|
for j, sol in enumerate(candidates[this_d.disjunction_id]):
|
|
testispartok = True
|
|
testispartnokloose = 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 any pth from sol does not contain the ordered list node,
|
|
# remove sol from the candidate, except if constraint was loose:
|
|
# then keep sol as an alternate solution
|
|
if not ispart(allpaths[id(pth)].req.nodes_list, pth):
|
|
testispartok = False
|
|
if 'STRICT' in allpaths[id(pth)].req.loose_list:
|
|
LOGGER.debug(f'removing solution from candidate paths\n{pth}')
|
|
testispartnokloose = False
|
|
break
|
|
if testispartok:
|
|
temp.append(sol)
|
|
elif testispartnokloose:
|
|
LOGGER.debug(f'Adding solution as alternate solution not satisfying constraint\n{pth}')
|
|
alternatetemp.append(sol)
|
|
if temp:
|
|
candidates[this_d.disjunction_id] = temp
|
|
elif alternatetemp:
|
|
candidates[this_d.disjunction_id] = alternatetemp
|
|
else:
|
|
candidates[this_d.disjunction_id] = []
|
|
|
|
# step 5 select the first combination that works
|
|
pathreslist_disjoint = {}
|
|
for dis in disjunctions_list:
|
|
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
|
|
# remove request from list of requests (in case of duplicate)
|
|
pathreqlist_disjt.remove(allpaths[id(pth)].req)
|
|
# remove duplicated candidates
|
|
candidates = remove_candidate(candidates, allpaths, allpaths[id(pth)].req, pth)
|
|
else:
|
|
msg = 'No disjoint path found with added constraint\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.'
|
|
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 \
|
|
req1.tx_power == req2.tx_power 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
|
|
If transponder mode is defined and identical, then also agregates demands.
|
|
"""
|
|
# 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) and\
|
|
this_r.tsp_mode is not None:
|
|
# aggregate
|
|
this_r.path_bandwidth += req.path_bandwidth
|
|
this_r.N = this_r.N + req.N
|
|
this_r.M = this_r.M + req.M
|
|
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
|
|
|
|
|
|
def correct_json_route_list(network, pathreqlist):
|
|
"""all names in list should be exact name in the network, and there is no ambiguity
|
|
|
|
This function only checks that list is correct, warns user if the name is incorrect and
|
|
suppresses the constraint it it is loose or raises an error if it is strict
|
|
"""
|
|
all_uid = [n.uid for n in network.nodes()]
|
|
transponders = [n.uid for n in network.nodes() if isinstance(n, Transceiver)]
|
|
for pathreq in pathreqlist:
|
|
if pathreq.source not in transponders:
|
|
msg = f'Request: {pathreq.request_id}: could not find transponder' \
|
|
+ f' source : {pathreq.source}.'
|
|
raise ServiceError(msg)
|
|
|
|
if pathreq.destination not in transponders:
|
|
msg = f'Request: {pathreq.request_id}: could not find transponder' \
|
|
+ f' destination : {pathreq.destination}.'
|
|
raise ServiceError(msg)
|
|
|
|
# silently remove source and dest nodes from the list
|
|
if pathreq.nodes_list and pathreq.source == pathreq.nodes_list[0]:
|
|
pathreq.loose_list.pop(0)
|
|
pathreq.nodes_list.pop(0)
|
|
if pathreq.nodes_list and pathreq.destination == pathreq.nodes_list[-1]:
|
|
pathreq.loose_list.pop(-1)
|
|
pathreq.nodes_list.pop(-1)
|
|
temp = deepcopy(pathreq)
|
|
for i, n_id in enumerate(temp.nodes_list):
|
|
# a node within this list must be part of the topology and should not be a transceiver,
|
|
# because only source and dest are transceivers
|
|
if n_id not in all_uid or n_id in transponders:
|
|
if temp.loose_list[i] == 'LOOSE':
|
|
# if no matching can be found in the network just ignore this constraint
|
|
# if it is a loose constraint
|
|
# warns the user that this node is not part of the topology
|
|
msg = f'invalid route node specified:\n\t\'{n_id}\',' \
|
|
+ ' could not use it as constraint, skipped!'
|
|
LOGGER.warning(msg)
|
|
pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
|
|
pathreq.nodes_list.remove(n_id)
|
|
else:
|
|
msg = f'could not find node:\n\t \'{n_id}\' in network' \
|
|
+ ' topology. Strict constraint can not be applied.'
|
|
raise ServiceError(msg)
|
|
|
|
return pathreqlist
|
|
|
|
|
|
def deduplicate_disjunctions(disjn):
|
|
"""clean disjunctions to remove possible repetition"""
|
|
local_disjn = disjn.copy()
|
|
for elem in local_disjn:
|
|
for dis_elem in local_disjn:
|
|
if set(elem.disjunctions_req) == set(dis_elem.disjunctions_req) and \
|
|
elem.disjunction_id != dis_elem.disjunction_id:
|
|
local_disjn.remove(dis_elem)
|
|
return local_disjn
|
|
|
|
|
|
def compute_path_with_disjunction(network, equipment, pathreqlist, pathlist, redesign=False):
|
|
"""use a list but a dictionnary might be helpful to find path based on request_id
|
|
|
|
TODO change all these req, dsjct, res lists into dict !
|
|
"""
|
|
path_res_list = []
|
|
reversed_path_res_list = []
|
|
propagated_reversed_path_res_list = []
|
|
|
|
total_nb_requests = len(pathreqlist)
|
|
if redesign:
|
|
LOGGER.warning('Redesign the network for each request channel, '
|
|
+ 'using the request channel as the reference channel for the design.')
|
|
for i, pathreq in enumerate(pathreqlist):
|
|
|
|
# use the power specified in requests but might be different from the one
|
|
# specified for design the power is an optional parameter for requests
|
|
# definition if optional, use the one defines in eqt_config.json
|
|
msg = f'\n\trequest {pathreq.request_id}\n' \
|
|
+ f'\tComputing path from {pathreq.source} to {pathreq.destination}\n' \
|
|
+ f'\twith path constraint: {[pathreq.source] + pathreq.nodes_list}'
|
|
# # adding first node to be clearer on the output
|
|
|
|
# pathlist[i] contains the whole path information for request i
|
|
# last element is a transciver and where the result of the propagation is
|
|
# recorded.
|
|
# Important Note: since transceivers attached to roadms are actually logical
|
|
# elements to simulate performance, several demands having the same destination
|
|
# may use the same transponder for the performance simulation. This is why
|
|
# we use deepcopy: to ensure that each propagation is recorded and not overwritten
|
|
# reversed path is needed for correct spectrum assignment
|
|
if redesign:
|
|
# this is the legacy case where network was automatically redesigned using the
|
|
# request channel as reference (nb and power used for amplifiers total power out)
|
|
reversed_path = []
|
|
if pathlist[i]:
|
|
reversed_path = find_reversed_path(pathlist[i])
|
|
network_nodes_for_redesign = pathlist[i] + reversed_path
|
|
network_module.design_network(pathreq, network.subgraph(network_nodes_for_redesign), equipment,
|
|
set_connector_losses=False, verbose=False)
|
|
total_path = deepcopy(pathlist[i])
|
|
msg = msg + f'\n\tComputed path (roadms):{[e.uid for e in total_path if isinstance(e, Roadm)]}'
|
|
LOGGER.info(msg)
|
|
# for debug
|
|
# print(f'{pathreq.baud_rate} {pathreq.power} {pathreq.spacing} {pathreq.nb_channel}')
|
|
if total_path:
|
|
if pathreq.baud_rate is not None:
|
|
# means that at this point the mode was entered/forced by user and thus a
|
|
# baud_rate was defined
|
|
propagate(total_path, pathreq, equipment)
|
|
snr01nm_with_penalty = total_path[-1].snr_01nm - total_path[-1].total_penalty
|
|
min_ind = argmin(snr01nm_with_penalty)
|
|
if round(snr01nm_with_penalty[min_ind], 2) < pathreq.OSNR + equipment['SI']['default'].sys_margins:
|
|
msg = f'\tWarning! Request {pathreq.request_id} computed path from' \
|
|
+ f' {pathreq.source} to {pathreq.destination} does not pass with {pathreq.tsp_mode}' \
|
|
+ f'\n\tcomputed SNR in 0.1nm = {round(total_path[-1].snr_01nm[min_ind], 2)}'
|
|
msg = _penalty_msg(total_path, msg, min_ind) \
|
|
+ f'\n\trequired osnr = {pathreq.OSNR}' \
|
|
+ f'\n\tsystem margin = {equipment["SI"]["default"].sys_margins}'
|
|
LOGGER.warning(msg)
|
|
pathreq.blocking_reason = 'MODE_NOT_FEASIBLE'
|
|
else:
|
|
total_path, mode = propagate_and_optimize_mode(total_path, pathreq, equipment)
|
|
# if no baudrate satisfies spacing, no mode is returned and the last explored mode
|
|
# a warning is shown in the propagate_and_optimize_mode
|
|
# propagate_and_optimize_mode function returns the mode with the highest bitrate
|
|
# that passes. if no mode passes, then a attribute blocking_reason is added on
|
|
# pathreq that contains the reason for blocking: 'NO_PATH', 'NO_FEASIBLE_MODE', ...
|
|
try:
|
|
if pathreq.blocking_reason in BLOCKING_NOPATH:
|
|
total_path = []
|
|
elif pathreq.blocking_reason in BLOCKING_NOMODE:
|
|
pathreq.baud_rate = mode['baud_rate']
|
|
pathreq.tsp_mode = mode['format']
|
|
pathreq.format = mode['format']
|
|
pathreq.OSNR = mode['OSNR']
|
|
pathreq.tx_osnr = mode['tx_osnr']
|
|
pathreq.bit_rate = mode['bit_rate']
|
|
pathreq.penalties = mode['penalties']
|
|
pathreq.offset_db = mode['equalization_offset_db']
|
|
# other blocking reason should not appear at this point
|
|
except AttributeError:
|
|
pathreq.baud_rate = mode['baud_rate']
|
|
pathreq.tsp_mode = mode['format']
|
|
pathreq.format = mode['format']
|
|
pathreq.OSNR = mode['OSNR']
|
|
pathreq.tx_osnr = mode['tx_osnr']
|
|
pathreq.bit_rate = mode['bit_rate']
|
|
pathreq.penalties = mode['penalties']
|
|
pathreq.offset_db = mode['equalization_offset_db']
|
|
|
|
# reversed path is needed for correct spectrum assignment
|
|
reversed_path = find_reversed_path(pathlist[i])
|
|
if pathreq.bidir and pathreq.baud_rate is not None:
|
|
# Both directions requested, and a feasible mode was found
|
|
rev_p = deepcopy(reversed_path)
|
|
msg = f'\n\tPropagating Z to A direction {pathreq.destination} to {pathreq.source}\n' \
|
|
+ f'\tPath (roadms) {[r.uid for r in rev_p if isinstance(r,Roadm)]}\n'
|
|
LOGGER.info(msg)
|
|
propagate(rev_p, pathreq, equipment)
|
|
propagated_reversed_path = rev_p
|
|
snr01nm_with_penalty = rev_p[-1].snr_01nm - rev_p[-1].total_penalty
|
|
min_ind = argmin(snr01nm_with_penalty)
|
|
if round(snr01nm_with_penalty[min_ind], 2) < pathreq.OSNR + equipment['SI']['default'].sys_margins:
|
|
msg = f'\tWarning! Request {pathreq.request_id} computed path from' \
|
|
+ f' {pathreq.destination} to {pathreq.source} does not pass with {pathreq.tsp_mode}' \
|
|
+ f'\n\tcomputed SNR in 0.1nm = {round(rev_p[-1].snr_01nm[min_ind], 2)}'
|
|
msg = _penalty_msg(rev_p, msg, min_ind) \
|
|
+ f'\n\trequired osnr = {pathreq.OSNR}' \
|
|
+ f'\n\tsystem margin = {equipment["SI"]["default"].sys_margins}'
|
|
LOGGER.warning(msg)
|
|
# TODO selection of mode should also be on reversed direction !!
|
|
if not hasattr(pathreq, 'blocking_reason'):
|
|
pathreq.blocking_reason = 'MODE_NOT_FEASIBLE'
|
|
else:
|
|
propagated_reversed_path = []
|
|
else:
|
|
msg = f'Request {pathreq.request_id}: Total path is empty. No propagation'
|
|
LOGGER.warning(msg)
|
|
reversed_path = []
|
|
propagated_reversed_path = []
|
|
|
|
path_res_list.append(total_path)
|
|
reversed_path_res_list.append(reversed_path)
|
|
propagated_reversed_path_res_list.append(propagated_reversed_path)
|
|
# print to have a nice output
|
|
return path_res_list, reversed_path_res_list, propagated_reversed_path_res_list
|
|
|
|
|
|
def compute_spectrum_slot_vs_bandwidth(bandwidth, spacing, bit_rate, slot_width=0.0125e12):
|
|
"""Compute the number of required wavelengths and the M value (number of consumed slots)
|
|
|
|
Each wavelength consumes one `spacing`, and the result is rounded up to consume a natural number of slots.
|
|
|
|
>>> compute_spectrum_slot_vs_bandwidth(400e9, 50e9, 200e9)
|
|
(2, 8)
|
|
"""
|
|
number_of_wavelengths = ceil(bandwidth / bit_rate)
|
|
total_number_of_slots = ceil(spacing / slot_width) * number_of_wavelengths
|
|
return number_of_wavelengths, total_number_of_slots
|
|
|
|
|
|
def _penalty_msg(total_path, msg, min_ind):
|
|
"""formatting helper for reporting unfeasible paths
|
|
|
|
The penalty info are optional, so this checks that penalty exists before creating a message."""
|
|
penalty_dict = {
|
|
'pdl': 'PDL',
|
|
'chromatic_dispersion': 'CD',
|
|
'pmd': 'PMD'}
|
|
for key, pretty in penalty_dict.items():
|
|
if key in total_path[-1].penalties:
|
|
msg += f'\n\t{pretty} penalty = {round(total_path[-1].penalties[key][min_ind], 2)}'
|
|
else:
|
|
msg += f'\n\t{pretty} penalty not evaluated'
|
|
return msg
|
|
|
|
|
|
def is_adjacent(oms1, oms2):
|
|
""" oms1's egress ROADM is oms2's ingress ROADM
|
|
"""
|
|
return oms1.el_list[-1] == oms2.el_list[0]
|
|
|
|
|
|
def explicit_path(node_list, source, destination, network):
|
|
""" if list of nodes leads to adjacent oms, then means that the path is explicit, and no need to compute
|
|
the function returns the explicit path (including source and destination ROADMs)
|
|
"""
|
|
path_oms = []
|
|
for elem in node_list:
|
|
if hasattr(elem, 'oms'):
|
|
path_oms.append(elem.oms)
|
|
if not path_oms:
|
|
return None
|
|
path_oms = unique_ordered(path_oms)
|
|
try:
|
|
next_node = next(network.successors(source))
|
|
source_roadm = next_node if isinstance(next_node, Roadm) else source
|
|
previous_node = next(network.predecessors(destination))
|
|
destination_roadm = previous_node if isinstance(previous_node, Roadm) else destination
|
|
if not (path_oms[0].el_list[0] == source_roadm and path_oms[-1].el_list[-1] == destination_roadm):
|
|
return None
|
|
except StopIteration:
|
|
return None
|
|
|
|
oms0 = path_oms[0]
|
|
path = [source] + oms0.el_list
|
|
for oms in path_oms[1:]:
|
|
if not is_adjacent(oms0, oms):
|
|
return None
|
|
oms0 = oms
|
|
path.extend(oms.el_list)
|
|
path.append(destination)
|
|
return unique_ordered(path)
|
|
|
|
|
|
def find_elements_common_range(el_list: list, equipment: dict) -> List[dict]:
|
|
"""Find the common frequency range of amps of a given list of elements (for example an OMS or a path)
|
|
If there are no amplifiers in the path, then use the SI
|
|
"""
|
|
amp_bands = [n.params.bands for n in el_list if isinstance(n, (Edfa, Multiband_amplifier))]
|
|
return find_common_range(amp_bands, equipment['SI']['default'].f_min, equipment['SI']['default'].f_max,
|
|
equipment['SI']['default'].spacing)
|