mirror of
https://github.com/Telecominfraproject/oopt-gnpy.git
synced 2025-10-30 17:47:50 +00:00
I'm trying to clean up the core code while not causing too much trouble to our users. Esther pointed out that there's an internal use case at Orange where people are looking at the incremental results when computing paths. I strongly dislike commented-out debugging code, but for the sake of progress, let's put it in here. It's roughly as good as that unused `show` parameter, and it allowed a refactoring to make the code more readable.
370 lines
18 KiB
Python
Executable File
370 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
path_requests_run.py
|
|
====================
|
|
|
|
Reads a JSON request file in accordance with the Yang model
|
|
for requesting path computation and returns path results in terms
|
|
of path and feasibilty.
|
|
|
|
See: draft-ietf-teas-yang-path-computation-01.txt
|
|
"""
|
|
|
|
from sys import exit
|
|
from argparse import ArgumentParser
|
|
from pathlib import Path
|
|
from collections import namedtuple
|
|
from logging import getLogger, basicConfig, CRITICAL, DEBUG, INFO
|
|
from json import dumps, loads
|
|
from networkx import (draw_networkx_nodes, draw_networkx_edges,
|
|
draw_networkx_labels)
|
|
from numpy import mean
|
|
from gnpy.core.service_sheet import convert_service_sheet, Request_element, Element
|
|
from gnpy.core.utils import load_json
|
|
from gnpy.core.network import load_network, build_network, save_network
|
|
from gnpy.core.equipment import load_equipment, trx_mode_params, automatic_nch, automatic_spacing
|
|
from gnpy.core.elements import Transceiver, Roadm, Edfa, Fused, Fiber
|
|
from gnpy.core.utils import db2lin, lin2db
|
|
from gnpy.core.request import (Path_request, Result_element, compute_constrained_path,
|
|
propagate, jsontocsv, Disjunction, compute_path_dsjctn, requests_aggregation,
|
|
propagate_and_optimize_mode)
|
|
from gnpy.core.exceptions import ConfigurationError, EquipmentConfigError, NetworkTopologyError
|
|
import gnpy.core.ansi_escapes as ansi_escapes
|
|
from copy import copy, deepcopy
|
|
from textwrap import dedent
|
|
from math import ceil
|
|
|
|
#EQPT_LIBRARY_FILENAME = Path(__file__).parent / 'eqpt_config.json'
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
parser = ArgumentParser(description = 'A function that computes performances for a list of services provided in a json file or an excel sheet.')
|
|
parser.add_argument('network_filename', nargs='?', type = Path, default= Path(__file__).parent / 'meshTopologyExampleV2.xls')
|
|
parser.add_argument('service_filename', nargs='?', type = Path, default= Path(__file__).parent / 'meshTopologyExampleV2.xls')
|
|
parser.add_argument('eqpt_filename', nargs='?', type = Path, default=Path(__file__).parent / 'eqpt_config.json')
|
|
parser.add_argument('-v', '--verbose', action='count', default=0, help='increases verbosity for each occurence')
|
|
parser.add_argument('-o', '--output', type = Path)
|
|
|
|
|
|
def requests_from_json(json_data,equipment):
|
|
requests_list = []
|
|
|
|
for req in json_data['path-request']:
|
|
# init all params from request
|
|
params = {}
|
|
params['request_id'] = req['request-id']
|
|
params['source'] = req['src-tp-id']
|
|
params['destination'] = req['dst-tp-id']
|
|
params['trx_type'] = req['path-constraints']['te-bandwidth']['trx_type']
|
|
params['trx_mode'] = req['path-constraints']['te-bandwidth']['trx_mode']
|
|
params['format'] = params['trx_mode']
|
|
nd_list = req['optimizations']['explicit-route-include-objects']
|
|
params['nodes_list'] = [n['unnumbered-hop']['node-id'] for n in nd_list]
|
|
params['loose_list'] = [n['unnumbered-hop']['hop-type'] for n in nd_list]
|
|
params['spacing'] = req['path-constraints']['te-bandwidth']['spacing']
|
|
|
|
# recover trx physical param (baudrate, ...) from type and mode
|
|
# in trx_mode_params optical power is read from equipment['SI']['default'] and
|
|
# nb_channel is computed based on min max frequency and spacing
|
|
trx_params = trx_mode_params(equipment,params['trx_type'],params['trx_mode'],True)
|
|
params.update(trx_params)
|
|
# print(trx_params['min_spacing'])
|
|
# optical power might be set differently in the request. if it is indicated then the
|
|
# params['power'] is updated
|
|
if req['path-constraints']['te-bandwidth']['output-power']:
|
|
params['power'] = req['path-constraints']['te-bandwidth']['output-power']
|
|
|
|
# same process for nb-channel
|
|
f_min = params['f_min']
|
|
f_max_from_si = params['f_max']
|
|
if req['path-constraints']['te-bandwidth']['max-nb-of-channel'] is not None :
|
|
nch = req['path-constraints']['te-bandwidth']['max-nb-of-channel']
|
|
params['nb_channel'] = nch
|
|
spacing = params['spacing']
|
|
params['f_max'] = f_min + nch*spacing
|
|
else :
|
|
params['nb_channel'] = automatic_nch(f_min,f_max_from_si,params['spacing'])
|
|
|
|
consistency_check(params, f_max_from_si)
|
|
|
|
try :
|
|
params['path_bandwidth'] = req['path-constraints']['te-bandwidth']['path_bandwidth']
|
|
except KeyError:
|
|
pass
|
|
requests_list.append(Path_request(**params))
|
|
return requests_list
|
|
|
|
def consistency_check(params, f_max_from_si):
|
|
f_min = params['f_min']
|
|
f_max = params['f_max']
|
|
max_recommanded_nb_channels = automatic_nch(f_min,f_max,
|
|
params['spacing'])
|
|
if params['baud_rate'] is not None:
|
|
#implicitely means that a mode is defined with min_spacing
|
|
if params['min_spacing']>params['spacing'] :
|
|
msg = f'Request {params["request_id"]} has spacing below transponder {params["trx_type"]}'+\
|
|
f' {params["trx_mode"]} min spacing value {params["min_spacing"]*1e-9}GHz.\n'+\
|
|
'Computation stopped'
|
|
print(msg)
|
|
logger.critical(msg)
|
|
exit()
|
|
if f_max>f_max_from_si:
|
|
msg = dedent(f'''
|
|
Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"]} GHz and requested spacing {params["spacing"]*1e-9}GHz
|
|
is not consistent with frequency range {f_min*1e-12} THz, {f_max*1e-12} THz, min recommanded spacing {params["min_spacing"]*1e-9}GHz.
|
|
max recommanded nb of channels is {max_recommanded_nb_channels}
|
|
Computation stopped.''')
|
|
logger.critical(msg)
|
|
exit()
|
|
|
|
|
|
def disjunctions_from_json(json_data):
|
|
disjunctions_list = []
|
|
|
|
for snc in json_data['synchronization']:
|
|
params = {}
|
|
params['disjunction_id'] = snc['synchronization-id']
|
|
params['relaxable'] = snc['svec']['relaxable']
|
|
params['link_diverse'] = snc['svec']['link-diverse']
|
|
params['node_diverse'] = snc['svec']['node-diverse']
|
|
params['disjunctions_req'] = snc['svec']['request-id-number']
|
|
disjunctions_list.append(Disjunction(**params))
|
|
return disjunctions_list
|
|
|
|
|
|
def load_requests(filename,eqpt_filename):
|
|
if filename.suffix.lower() == '.xls':
|
|
logger.info('Automatically converting requests from XLS to JSON')
|
|
json_data = convert_service_sheet(filename,eqpt_filename)
|
|
else:
|
|
with open(filename, encoding='utf-8') as f:
|
|
json_data = loads(f.read())
|
|
return json_data
|
|
|
|
def compute_path_with_disjunction(network, equipment, pathreqlist, pathlist):
|
|
|
|
# use a list but a dictionnary might be helpful to find path bathsed on request_id
|
|
# TODO change all these req, dsjct, res lists into dict !
|
|
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
|
|
p_db = lin2db(pathreq.power*1e3)
|
|
p_total_db = p_db + lin2db(pathreq.nb_channel)
|
|
print(f'request {pathreq.request_id}')
|
|
print(f'Computing path from {pathreq.source} to {pathreq.destination}')
|
|
print(f'with path constraint: {[pathreq.source]+pathreq.nodes_list}') #adding first node to be clearer on the output
|
|
|
|
total_path = pathlist[i]
|
|
print(f'Computed path (roadms):{[e.uid for e in total_path if isinstance(e, Roadm)]}\n')
|
|
# for debug
|
|
# print(f'{pathreq.baud_rate} {pathreq.power} {pathreq.spacing} {pathreq.nb_channel}')
|
|
if total_path :
|
|
if pathreq.baud_rate is not None:
|
|
total_path = propagate(total_path,pathreq,equipment)
|
|
# for el in total_path: print(el)
|
|
temp_snr01nm = round(mean(total_path[-1].snr+lin2db(pathreq.baud_rate/(12.5e9))),2)
|
|
if temp_snr01nm < pathreq.OSNR :
|
|
msg = f'\tWarning! Request {pathreq.request_id} computed path from {pathreq.source} to {pathreq.destination} does not pass with {pathreq.tsp_mode}\n' +\
|
|
f'\tcomputedSNR in 0.1nm = {temp_snr01nm} - required osnr {pathreq.OSNR}\n'
|
|
print(msg)
|
|
logger.warning(msg)
|
|
total_path = []
|
|
else:
|
|
total_path,mode = propagate_and_optimize_mode(total_path,pathreq,equipment)
|
|
# if no baudrate satisfies spacing, no mode is returned and an empty path is returned
|
|
# a warning is shown in the propagate_and_optimize_mode
|
|
if mode is not None :
|
|
# propagate_and_optimize_mode function returns the mode with the highest bitrate
|
|
# that passes. if no mode passes, then it returns an empty path
|
|
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']
|
|
else :
|
|
total_path = []
|
|
# we record the last tranceiver object in order to have th whole
|
|
# information about spectrum. 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 simaulation. This is why
|
|
# we use deepcopy: to ensure each propagation is recorded and not
|
|
# overwritten
|
|
|
|
path_res_list.append(deepcopy(total_path))
|
|
return path_res_list
|
|
|
|
def correct_route_list(network, pathreqlist):
|
|
# prepares the format of route list of nodes to be consistant
|
|
# remove wrong names, remove endpoints
|
|
# also correct source and destination
|
|
anytype = [n.uid for n in network.nodes() if not isinstance(n, Transceiver) and not isinstance(n, Fiber)]
|
|
# TODO there is a problem of identification of fibers in case of parallel fibers bitween two adjacent roadms
|
|
# so fiber constraint is not supported
|
|
transponders = [n.uid for n in network.nodes() if isinstance(n, Transceiver)]
|
|
for pathreq in pathreqlist:
|
|
for i,n_id in enumerate(pathreq.nodes_list):
|
|
# replace possibly wrong name with a formated roadm name
|
|
# print(n_id)
|
|
if n_id not in anytype :
|
|
nodes_suggestion = [uid for uid in anytype \
|
|
if n_id.lower() in uid.lower()]
|
|
if pathreq.loose_list[i] == 'loose':
|
|
if len(nodes_suggestion)>0 :
|
|
new_n = nodes_suggestion[0]
|
|
print(f'invalid route node specified:\
|
|
\n\'{n_id}\', replaced with \'{new_n}\'')
|
|
pathreq.nodes_list[i] = new_n
|
|
else:
|
|
print(f'\x1b[1;33;40m'+f'invalid route node specified \'{n_id}\', could not use it as constraint, skipped!'+'\x1b[0m')
|
|
pathreq.nodes_list.remove(n_id)
|
|
pathreq.loose_list.pop(i)
|
|
else:
|
|
msg = f'\x1b[1;33;40m'+f'could not find node : {n_id} in network topology. Strict constraint can not be applied.'+'\x1b[0m'
|
|
logger.critical(msg)
|
|
raise ValueError(msg)
|
|
if pathreq.source not in transponders:
|
|
msg = f'\x1b[1;31;40m'+f'Request: {pathreq.request_id}: could not find transponder source : {pathreq.source}.'+'\x1b[0m'
|
|
logger.critical(msg)
|
|
print(f'{msg}\nComputation stopped.')
|
|
exit()
|
|
|
|
if pathreq.destination not in transponders:
|
|
msg = f'\x1b[1;31;40m'+f'Request: {pathreq.request_id}: could not find transponder destination : {pathreq.destination}.'+'\x1b[0m'
|
|
logger.critical(msg)
|
|
print(f'{msg}\nComputation stopped.')
|
|
exit()
|
|
|
|
# TODO remove endpoints from this list in case they were added by the user in the xls or json files
|
|
return pathreqlist
|
|
|
|
def correct_disjn(disjn):
|
|
local_disjn = disjn.copy()
|
|
for el in local_disjn:
|
|
for d in local_disjn:
|
|
if set(el.disjunctions_req) == set(d.disjunctions_req) and\
|
|
el.disjunction_id != d.disjunction_id:
|
|
local_disjn.remove(d)
|
|
return local_disjn
|
|
|
|
|
|
def path_result_json(pathresult):
|
|
data = {
|
|
'path': [n.json for n in pathresult]
|
|
}
|
|
return data
|
|
|
|
|
|
if __name__ == '__main__':
|
|
args = parser.parse_args()
|
|
basicConfig(level={2: DEBUG, 1: INFO, 0: CRITICAL}.get(args.verbose, DEBUG))
|
|
logger.info(f'Computing path requests {args.service_filename} into JSON format')
|
|
print('\x1b[1;34;40m'+f'Computing path requests {args.service_filename} into JSON format'+ '\x1b[0m')
|
|
# for debug
|
|
# print( args.eqpt_filename)
|
|
try:
|
|
data = load_requests(args.service_filename,args.eqpt_filename)
|
|
equipment = load_equipment(args.eqpt_filename)
|
|
network = load_network(args.network_filename,equipment)
|
|
except EquipmentConfigError as e:
|
|
print(f'{ansi_escapes.red}Configuration error in the equipment library:{ansi_escapes.reset} {e}')
|
|
exit(1)
|
|
except NetworkTopologyError as e:
|
|
print(f'{ansi_escapes.red}Invalid network definition:{ansi_escapes.reset} {e}')
|
|
exit(1)
|
|
except ConfigurationError as e:
|
|
print(f'{ansi_escapes.red}Configuration error:{ansi_escapes.reset} {e}')
|
|
exit(1)
|
|
|
|
# Build the network once using the default power defined in SI in eqpt config
|
|
# TODO power density : db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
|
|
# spacing, f_min and f_max
|
|
p_db = equipment['SI']['default'].power_dbm
|
|
|
|
p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,\
|
|
equipment['SI']['default'].f_max, equipment['SI']['default'].spacing))
|
|
build_network(network, equipment, p_db, p_total_db)
|
|
save_network(args.network_filename, network)
|
|
|
|
rqs = requests_from_json(data, equipment)
|
|
|
|
# check that request ids are unique. Non unique ids, may
|
|
# mess the computation : better to stop the computation
|
|
all_ids = [r.request_id for r in rqs]
|
|
if len(all_ids) != len(set(all_ids)):
|
|
for a in list(set(all_ids)):
|
|
all_ids.remove(a)
|
|
msg = f'Requests id {all_ids} are not unique'
|
|
logger.critical(msg)
|
|
exit()
|
|
rqs = correct_route_list(network, rqs)
|
|
|
|
# pths = compute_path(network, equipment, rqs)
|
|
dsjn = disjunctions_from_json(data)
|
|
|
|
print('\x1b[1;34;40m'+f'List of disjunctions'+ '\x1b[0m')
|
|
print(dsjn)
|
|
# need to warn or correct in case of wrong disjunction form
|
|
# disjunction must not be repeated with same or different ids
|
|
dsjn = correct_disjn(dsjn)
|
|
|
|
# Aggregate demands with same exact constraints
|
|
print('\x1b[1;34;40m'+f'Aggregating similar requests'+ '\x1b[0m')
|
|
|
|
rqs,dsjn = requests_aggregation(rqs,dsjn)
|
|
# TODO export novel set of aggregated demands in a json file
|
|
|
|
print('\x1b[1;34;40m'+'The following services have been requested:'+ '\x1b[0m')
|
|
print(rqs)
|
|
|
|
print('\x1b[1;34;40m'+f'Computing all paths with constraints'+ '\x1b[0m')
|
|
pths = compute_path_dsjctn(network, equipment, rqs, dsjn)
|
|
|
|
print('\x1b[1;34;40m'+f'Propagating on selected path'+ '\x1b[0m')
|
|
propagatedpths = compute_path_with_disjunction(network, equipment, rqs, pths)
|
|
|
|
print('\x1b[1;34;40m'+f'Result summary'+ '\x1b[0m')
|
|
|
|
header = ['req id', ' demand',' snr@bandwidth',' snr@0.1nm',' Receiver minOSNR', ' mode', ' Gbit/s' , ' nb of tsp pairs']
|
|
data = []
|
|
data.append(header)
|
|
for i, p in enumerate(propagatedpths):
|
|
if p:
|
|
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', f'{round(mean(p[-1].snr),2)}',\
|
|
f'{round(mean(p[-1].snr+lin2db(rqs[i].baud_rate/(12.5e9))),2)}',\
|
|
f'{rqs[i].OSNR}', f'{rqs[i].tsp_mode}' , f'{round(rqs[i].path_bandwidth * 1e-9,2)}' , f'{ceil(rqs[i].path_bandwidth / rqs[i].bit_rate) }']
|
|
else:
|
|
line = [f'{rqs[i].request_id}',f' {rqs[i].source} to {rqs[i].destination} : not feasible ']
|
|
data.append(line)
|
|
|
|
col_width = max(len(word) for row in data for word in row[2:]) # padding
|
|
firstcol_width = max(len(row[0]) for row in data ) # padding
|
|
secondcol_width = max(len(row[1]) for row in data ) # padding
|
|
for row in data:
|
|
firstcol = ''.join(row[0].ljust(firstcol_width))
|
|
secondcol = ''.join(row[1].ljust(secondcol_width))
|
|
remainingcols = ''.join(word.center(col_width,' ') for word in row[2:])
|
|
print(f'{firstcol} {secondcol} {remainingcols}')
|
|
|
|
|
|
if args.output :
|
|
result = []
|
|
# assumes that list of rqs and list of propgatedpths have same order
|
|
for i,p in enumerate(propagatedpths):
|
|
result.append(Result_element(rqs[i],p))
|
|
temp = path_result_json(result)
|
|
fnamecsv = f'{str(args.output)[0:len(str(args.output))-len(str(args.output.suffix))]}.csv'
|
|
fnamejson = f'{str(args.output)[0:len(str(args.output))-len(str(args.output.suffix))]}.json'
|
|
with open(fnamejson, 'w', encoding='utf-8') as f:
|
|
f.write(dumps(path_result_json(result), indent=2, ensure_ascii=False))
|
|
with open(fnamecsv,"w", encoding='utf-8') as fcsv :
|
|
jsontocsv(temp,equipment,fcsv)
|
|
print('\x1b[1;34;40m'+f'saving in {args.output} and {fnamecsv}'+ '\x1b[0m')
|