mirror of
				https://github.com/Telecominfraproject/oopt-gnpy.git
				synced 2025-11-03 03:28:04 +00:00 
			
		
		
		
	When mode is not given and propagation is performed bidir, penalties corresponding to automatically selected mode are not correctly initialized in request, and gnpy-path-request fails. Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com> Change-Id: If045624ff5dad7f0dfdec93eaa05bb5eae86e643
		
			
				
	
	
		
			1208 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1208 lines
		
	
	
		
			55 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
# -*- coding: utf-8 -*-
 | 
						|
 | 
						|
"""
 | 
						|
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 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
 | 
						|
from gnpy.core.utils import lin2db
 | 
						|
from gnpy.core.info import create_input_spectral_information
 | 
						|
from gnpy.core.exceptions import ServiceError, DisjunctionError
 | 
						|
import gnpy.core.ansi_escapes as ansi_escapes
 | 
						|
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')
 | 
						|
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.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 = params.effective_freq_slot['N']
 | 
						|
            self.M = params.effective_freq_slot['M']
 | 
						|
 | 
						|
    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'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": self.path_request.N,
 | 
						|
                            "M": 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')
 | 
						|
        LOGGER.critical(msg)
 | 
						|
        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))
 | 
						|
 | 
						|
    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'{ansi_escapes.yellow}Request {req.request_id} could not find a path from'
 | 
						|
               f' {source.uid} to node: {destination.uid} in network topology{ansi_escapes.reset}')
 | 
						|
        LOGGER.critical(msg)
 | 
						|
        print(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
 | 
						|
        print(f'{ansi_escapes.yellow}Request {req.request_id} could not find a path crossing '
 | 
						|
              f'{[el.uid for el in nodes_list[:-1]]} in network topology{ansi_escapes.reset}')
 | 
						|
 | 
						|
        if 'STRICT' not in req.loose_list[:-1]:
 | 
						|
            msg = (f'{ansi_escapes.yellow}Request {req.request_id} could not find a path with user_'
 | 
						|
                   f'include node constraints{ansi_escapes.reset}')
 | 
						|
            LOGGER.info(msg)
 | 
						|
            print(f'constraint ignored')
 | 
						|
            total_path = dijkstra_path(network, source, destination, weight='weight')
 | 
						|
        else:
 | 
						|
            # one STRICT makes the whole list STRICT
 | 
						|
            msg = (f'{ansi_escapes.yellow}Request {req.request_id} could not find a path with user '
 | 
						|
                   f'include node constraints.\nNo path computed{ansi_escapes.reset}')
 | 
						|
            LOGGER.critical(msg)
 | 
						|
            print(msg)
 | 
						|
            req.blocking_reason = 'NO_PATH_WITH_CONSTRAINT'
 | 
						|
            total_path = []
 | 
						|
 | 
						|
    return total_path
 | 
						|
 | 
						|
 | 
						|
def propagate(path, req, equipment):
 | 
						|
    si = create_input_spectral_information(
 | 
						|
        req.f_min, req.f_max, req.roll_off, req.baud_rate,
 | 
						|
        req.power, req.spacing)
 | 
						|
    for i, el in enumerate(path):
 | 
						|
        if isinstance(el, Roadm):
 | 
						|
            si = el(si, degree=path[i+1].uid)
 | 
						|
        else:
 | 
						|
            si = el(si)
 | 
						|
    path[0].update_snr(req.tx_osnr)
 | 
						|
    path[0].calc_penalties(req.penalties)
 | 
						|
    if any(isinstance(el, Roadm) for el in path):
 | 
						|
        path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr)
 | 
						|
    else:
 | 
						|
        path[-1].update_snr(req.tx_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
 | 
						|
    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
 | 
						|
            # 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 i, el in enumerate(path):
 | 
						|
                if isinstance(el, Roadm):
 | 
						|
                    spc_info = el(spc_info, 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'])
 | 
						|
                    if any(isinstance(el, Roadm) for el in path):
 | 
						|
                        path[-1].update_snr(this_mode['tx_osnr'], equipment['Roadm']['default'].add_drop_osnr)
 | 
						|
                    else:
 | 
						|
                        path[-1].update_snr(this_mode['tx_osnr'])
 | 
						|
                    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'
 | 
						|
        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 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.info(f'removing solution from candidate paths\n{pth}')
 | 
						|
                            testispartnokloose = False
 | 
						|
                            break
 | 
						|
            if testispartok:
 | 
						|
                temp.append(sol)
 | 
						|
            elif testispartnokloose:
 | 
						|
                LOGGER.info(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 = 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 and \
 | 
						|
            getattr(req1, 'N', None) is None and getattr(req2, 'N', None) is None and \
 | 
						|
            getattr(req1, 'M', None) is None and getattr(req2, 'M', None) is None:
 | 
						|
        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
 | 
						|
 | 
						|
 | 
						|
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'{ansi_escapes.red}Request: {pathreq.request_id}: could not find transponder' +\
 | 
						|
                f' source : {pathreq.source}.{ansi_escapes.reset}'
 | 
						|
            LOGGER.critical(msg)
 | 
						|
            raise ServiceError(msg)
 | 
						|
 | 
						|
        if pathreq.destination not in transponders:
 | 
						|
            msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find transponder' +\
 | 
						|
                f' destination : {pathreq.destination}.{ansi_escapes.reset}'
 | 
						|
            LOGGER.critical(msg)
 | 
						|
            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'{ansi_escapes.yellow}invalid route node specified:\n\t\'{n_id}\',' +\
 | 
						|
                        f' could not use it as constraint, skipped!{ansi_escapes.reset}'
 | 
						|
                    print(msg)
 | 
						|
                    LOGGER.info(msg)
 | 
						|
                    pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
 | 
						|
                    pathreq.nodes_list.remove(n_id)
 | 
						|
                else:
 | 
						|
                    msg = f'{ansi_escapes.red}could not find node:\n\t \'{n_id}\' in network' +\
 | 
						|
                        f' topology. Strict constraint can not be applied.{ansi_escapes.reset}'
 | 
						|
                    LOGGER.critical(msg)
 | 
						|
                    raise ServiceError(msg)
 | 
						|
 | 
						|
    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):
 | 
						|
    """ 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 = []
 | 
						|
 | 
						|
    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
 | 
						|
        print(f'request {pathreq.request_id}')
 | 
						|
        print(f'Computing path from {pathreq.source} to {pathreq.destination}')
 | 
						|
        # adding first node to be clearer on the output
 | 
						|
        print(f'with path constraint: {[pathreq.source] + pathreq.nodes_list}')
 | 
						|
 | 
						|
        # 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
 | 
						|
        total_path = deepcopy(pathlist[i])
 | 
						|
        print(f'Computed path (roadms):{[e.uid for e in total_path  if isinstance(e, Roadm)]}')
 | 
						|
        # 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)}' +\
 | 
						|
                          f'\n\tCD penalty = {round(total_path[-1].penalties["chromatic_dispersion"][min_ind], 2)}' +\
 | 
						|
                          f'\n\tPMD penalty = {round(total_path[-1].penalties["pmd"][min_ind], 2)}' +\
 | 
						|
                          f'\n\trequired osnr = {pathreq.OSNR}' +\
 | 
						|
                          f'\n\tsystem margin = {equipment["SI"]["default"].sys_margins}'
 | 
						|
                    print(msg)
 | 
						|
                    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']
 | 
						|
                    # 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']
 | 
						|
 | 
						|
            # 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)
 | 
						|
 | 
						|
                print(f'\n\tPropagating Z to A direction {pathreq.destination} to {pathreq.source}')
 | 
						|
                print(f'\tPath (roadsm) {[r.uid for r in rev_p if isinstance(r,Roadm)]}\n')
 | 
						|
                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.source} to {pathreq.destination} does not pass with {pathreq.tsp_mode}' +\
 | 
						|
                          f'\n\tcomputed SNR in 0.1nm = {round(rev_p[-1].snr_01nm[min_ind], 2)}' +\
 | 
						|
                          f'\n\tCD penalty = {round(rev_p[-1].penalties["chromatic_dispersion"][min_ind], 2)}' +\
 | 
						|
                          f'\n\tPMD penalty = {round(rev_p[-1].penalties["pmd"][min_ind], 2)}' +\
 | 
						|
                          f'\n\trequired osnr = {pathreq.OSNR}' +\
 | 
						|
                          f'\n\tsystem margin = {equipment["SI"]["default"].sys_margins}'
 | 
						|
                    print(msg)
 | 
						|
                    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 = 'Total path is empty. No propagation'
 | 
						|
            print(msg)
 | 
						|
            LOGGER.info(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
 | 
						|
        print('')
 | 
						|
    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
 |