Feat: add offset power option for transceivers

Offset power is used for equalization purpose to correct for the
generic equalization target set in ROADM for this particular transceiver.
This is usefull to handle exception to the general equalization rule.
For example in the case of constant power equalization, the user might
want to apply particular power offsets unrelated to slot width or baudrate.
or in constant PSW, the user might want to have a given mode equalized for
a different value than the one computed based on the request bandwidth.

For example consider that a transceiver mode is meant to be equalized with
75 GHz whatever the spacing specified in request. then the user may specify
2 flavours depending on used spacing:

  service 1 : mode 3, spacing 75GHz
  service 2 : mode 4, spacing 87.5Ghz
avec
  {
    "format": "mode 3",
    "baud_rate": 64e9,
    "OSNR": 18,
    "bit_rate": 200e9,
    "roll_off": 0.15,
    "tx_osnr": 40,
    "min_spacing": 75e9,
    "cost": 1
  }

  {
    "format": "mode 4",
    "baud_rate": 64e9,
    "OSNR": 18,
    "bit_rate": 200e9,
    "roll_off": 0.15,
    "tx_osnr": 40,
    "min_spacing": 87.5e9,
    "equalization_offset_db": -0.67,
    "cost": 1
  }

then the same target power would be considered for mode3 and mode4
despite using a different slot width

Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
Change-Id: I437f75c42f257b88b24207260ef9ec9b1ab7066e
This commit is contained in:
EstherLerouzic
2022-10-28 09:02:44 +02:00
parent 33cc11b85c
commit a3edb20142
9 changed files with 284 additions and 23 deletions

View File

@@ -33,6 +33,7 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
+ f' mode "{trx_params["format"]}" has baud rate'
+ f' {trx_params["baud_rate"] * 1e-9:.3f} GHz greater than min_spacing'
+ f' {trx_params["min_spacing"] * 1e-9:.3f}.')
trx_params['equalization_offset_db'] = trx_params.get('equalization_offset_db', 0)
else:
mode_params = {"format": "undetermined",
"baud_rate": None,
@@ -42,7 +43,8 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
"roll_off": None,
"tx_osnr": None,
"min_spacing": None,
"cost": None}
"cost": None,
"equalization_offset_db": 0}
trx_params = {**mode_params}
trx_params['f_min'] = equipment['Transceiver'][trx_type_variety].frequency['min']
trx_params['f_max'] = equipment['Transceiver'][trx_type_variety].frequency['max']
@@ -68,6 +70,7 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
trx_params['roll_off'] = default_si_data.roll_off
trx_params['tx_osnr'] = default_si_data.tx_osnr
trx_params['min_spacing'] = None
trx_params['equalization_offset_db'] = 0
trx_params['power'] = db2lin(default_si_data.power_dbm) * 1e-3

View File

@@ -322,14 +322,15 @@ def create_arbitrary_spectral_information(frequency: Union[ndarray, Iterable, fl
raise
def create_input_spectral_information(f_min, f_max, roll_off, baud_rate, power, spacing, tx_osnr, ref_carrier=None):
def create_input_spectral_information(f_min, f_max, roll_off, baud_rate, power, spacing, tx_osnr, delta_pdb=0,
ref_carrier=None):
"""Creates a fixed slot width spectral information with flat power.
all arguments are scalar values"""
number_of_channels = automatic_nch(f_min, f_max, spacing)
frequency = [(f_min + spacing * i) for i in range(1, number_of_channels + 1)]
p_span0 = watt2dbm(power)
p_spani = watt2dbm(power)
delta_pdb_per_channel = zeros(number_of_channels)
delta_pdb_per_channel = delta_pdb * ones(number_of_channels)
label = [f'{baud_rate * 1e-9 :.2f}G' for i in range(number_of_channels)]
return create_arbitrary_spectral_information(frequency, slot_width=spacing, signal=power, baud_rate=baud_rate,
roll_off=roll_off, delta_pdb_per_channel=delta_pdb_per_channel,

View File

@@ -331,7 +331,6 @@ def path_requests_run(args=None):
# 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))
try:

View File

@@ -133,6 +133,7 @@ class Transceiver(_JsonThing):
for mode_params in self.mode:
penalties = mode_params.get('penalties')
mode_params['penalties'] = {}
mode_params['equalization_offset_db'] = mode_params.get('equalization_offset_db', 0)
if not penalties:
continue
for impairment in ('chromatic_dispersion', 'pmd', 'pdl'):
@@ -574,7 +575,6 @@ def requests_from_json(json_data, equipment):
# 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
try:

View File

@@ -35,14 +35,14 @@ 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')
' roll_off tx_osnr min_spacing cost path_bandwidth effective_freq_slot'
' equalization_offset_db')
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
@@ -72,6 +72,7 @@ class PathRequest:
self.N = params.effective_freq_slot['N']
self.M = params.effective_freq_slot['M']
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}',
@@ -351,7 +352,8 @@ def propagate(path, req, equipment):
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,
power=req.power, spacing=req.spacing, tx_osnr=req.tx_osnr, ref_carrier=ref_carrier(equipment))
power=req.power, spacing=req.spacing, tx_osnr=req.tx_osnr, delta_pdb=req.offset_db,
ref_carrier=ref_carrier(equipment))
for i, el in enumerate(path):
if isinstance(el, Roadm):
si = el(si, degree=path[i+1].uid)
@@ -369,20 +371,21 @@ def propagate(path, req, equipment):
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]))
# 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_to_explore = sorted(baudrate_to_explore, reverse=True)
if baudrate_to_explore:
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 in baudrate_to_explore:
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]
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)
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
@@ -394,6 +397,7 @@ def propagate_and_optimize_mode(path, req, equipment):
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, power=req.power, spacing=req.spacing,
delta_pdb=this_offset,
tx_osnr=req.tx_osnr, ref_carrier=ref_carrier(equipment))
for i, el in enumerate(path):
if isinstance(el, Roadm):

View File

@@ -217,7 +217,7 @@
"tx_osnr": 45,
"min_spacing": 75e9,
"cost":1
},
},
{
"format": "mode 4",
"baud_rate": 66e9,

View File

@@ -12,6 +12,7 @@ from pathlib import Path
import pytest
from numpy.testing import assert_allclose, assert_array_equal, assert_raises
from numpy import array
from copy import deepcopy
from gnpy.core.utils import lin2db, automatic_nch, dbm2watt, power_dbm_to_psd_mw_ghz, watt2dbm, psd2powerdbm
from gnpy.core.network import build_network
@@ -20,8 +21,9 @@ from gnpy.core.info import create_input_spectral_information, Pref, create_arbit
ReferenceCarrier
from gnpy.core.equipment import trx_mode_params
from gnpy.core.exceptions import ConfigurationError
from gnpy.tools.json_io import network_from_json, load_equipment, load_network, _spectrum_from_json, load_json
from gnpy.topology.request import PathRequest, compute_constrained_path, propagate
from gnpy.tools.json_io import network_from_json, load_equipment, load_network, _spectrum_from_json, load_json, \
Transceiver, requests_from_json
from gnpy.topology.request import PathRequest, compute_constrained_path, propagate, propagate_and_optimize_mode
TEST_DIR = Path(__file__).parent
@@ -583,3 +585,253 @@ def test_power_option(req_power):
assert_array_equal(infos_expected.pmd, infos_actual.pmd)
assert_array_equal(infos_expected.channel_number, infos_actual.channel_number)
assert_array_equal(infos_expected.number_of_channels, infos_actual.number_of_channels)
def transceiver(slot_width, value):
return {
"type_variety": "test_offset",
"frequency": {
"min": 191.3e12,
"max": 196.1e12
},
"mode": [
{
"format": "mode 1",
"baud_rate": 64e9,
"OSNR": 18,
"bit_rate": 100e9,
"roll_off": 0.15,
"tx_osnr": 40,
"min_spacing": 75e9,
"cost": 1
},
{
"format": "mode 3",
"baud_rate": 64e9,
"OSNR": 18,
"bit_rate": 100e9,
"roll_off": 0.15,
"tx_osnr": 40,
"min_spacing": slot_width,
"equalization_offset_db": value,
"cost": 1
}
]
}
def some_requests():
route = {
"route-object-include-exclude": [
{
"explicit-route-usage": "route-include-ero",
"index": 0,
"num-unnum-hop": {
"node-id": "trx Brest_KLA",
"link-tp-id": "link-tp-id is not used",
"hop-type": "STRICT"
}
},
{
"explicit-route-usage": "route-include-ero",
"index": 1,
"num-unnum-hop": {
"node-id": "trx Vannes_KBE",
"link-tp-id": "link-tp-id is not used",
"hop-type": "STRICT"
}
}
]
}
return {
"path-request": [{
"request-id": "2",
"source": "trx Brest_KLA",
"destination": "trx Vannes_KBE",
"src-tp-id": "trx Brest_KLA",
"dst-tp-id": "trx Vannes_KBE",
"bidirectional": False,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "test_offset",
"trx_mode": "mode 1",
"spacing": 75000000000.0,
"path_bandwidth": 100000000000.0
}
},
"explicit-route-objects": route
}, {
"request-id": "3",
"source": "trx Brest_KLA",
"destination": "trx Vannes_KBE",
"src-tp-id": "trx Brest_KLA",
"dst-tp-id": "trx Vannes_KBE",
"bidirectional": False,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "test_offset",
"trx_mode": "mode 3",
"spacing": 87500000000.0,
"path_bandwidth": 100000000000.0
}
},
"explicit-route-objects": route
}, {
"request-id": "4",
"source": "trx Brest_KLA",
"destination": "trx Vannes_KBE",
"src-tp-id": "trx Brest_KLA",
"dst-tp-id": "trx Vannes_KBE",
"bidirectional": False,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "test_offset",
"trx_mode": "mode 1",
"spacing": 87500000000.0,
"path_bandwidth": 100000000000.0
}
},
"explicit-route-objects": route
}]
}
@pytest.mark.parametrize('slot_width, value', [(75e9, lin2db(75 / 87.5)),
(87.5e9, lin2db(75 / 87.5))])
def test_power_offset_trx_equalization_psw(slot_width, value):
"""Check that the equalization with the offset is giving the same result as with reference slot_width
Check that larger slot width but no offset takes larger slot width for equalization
"""
equipment = load_equipment(EQPT_FILENAME)
trx = transceiver(slot_width, value)
equipment['Transceiver'][trx['type_variety']] = Transceiver(**trx)
setattr(equipment['Roadm']['default'], 'target_pch_out_db', None)
setattr(equipment['Roadm']['default'], 'target_out_mWperSlotWidth', power_dbm_to_psd_mw_ghz(-20, 50e9))
network = net_setup(equipment)
json_data = some_requests()
ref_request, request, other = requests_from_json(json_data, equipment)
# ref_request (_expected) has no offset, equalization on 75GH basis
path_expected = compute_constrained_path(network, ref_request)
_ = propagate(path_expected, ref_request, equipment)
roadm1_expected = deepcopy(path_expected[1])
# request has an offset either defined in power and a larger slot width.
# The defined offset is "equalize as if it was a 75 GHz channel" although slot_width is 87.5GHz
path = compute_constrained_path(network, request)
_ = propagate(path, request, equipment)
roadm1 = deepcopy(path[1])
# the other request has a larger slot width (spacing) but no offset. so equalization uses this slot width
path_other = compute_constrained_path(network, other)
_ = propagate(path, other, equipment)
roadm1_other = path_other[1]
# check the first frequency since all cariers have the same equalization
# Check that the power is equalized as if it was for a 75GHz channel (mode 1) instead of a 87.5GHz
assert roadm1.pch_out_dbm[0] == roadm1_expected.pch_out_dbm[0]
# Check that equalization instead uses 87.5GHz basis
assert roadm1_other.pch_out_dbm[0] == roadm1_expected.pch_out_dbm[0] + lin2db(87.5 / 75)
@pytest.mark.parametrize('slot_width, value', [(75e9, lin2db(75 / 50)),
(87.5e9, lin2db(75 / 50))])
def test_power_offset_trx_equalization_p(slot_width, value):
"""Check that the constant power equalization with the offset is applied
"""
equipment = load_equipment(EQPT_FILENAME)
trx = transceiver(slot_width, value)
equipment['Transceiver'][trx['type_variety']] = Transceiver(**trx)
setattr(equipment['Roadm']['default'], 'target_pch_out_db', -20)
network = net_setup(equipment)
json_data = some_requests()
ref_request, request, _ = requests_from_json(json_data, equipment)
path_expected = compute_constrained_path(network, ref_request)
_ = propagate(path_expected, ref_request, equipment)
roadm1_expected = deepcopy(path_expected[1])
path = compute_constrained_path(network, request)
_ = propagate(path, request, equipment)
roadm1 = deepcopy(path[1])
assert roadm1.pch_out_dbm[0] == roadm1_expected.pch_out_dbm[0] + lin2db(75 / 50)
@pytest.mark.parametrize('equalization, target_value',
[('target_pch_out_db', -20),
('target_psd_out_mWperGHz', power_dbm_to_psd_mw_ghz(-20, 64e9)),
('target_out_mWperSlotWidth', power_dbm_to_psd_mw_ghz(-20, 50e9))])
@pytest.mark.parametrize('slot_width, value, expected_mode', [(75e9, 3.0, 'mode 3')])
def test_power_offset_automatic_mode_selection(slot_width, value, equalization,
target_value, expected_mode):
"""Check that the same result is obtained if the mode is user defined or if it is
automatically selected
"""
equipment = load_equipment(EQPT_FILENAME)
trx = transceiver(slot_width, value)
equipment['Transceiver'][trx['type_variety']] = Transceiver(**trx)
setattr(equipment['Roadm']['default'], 'target_pch_out_db', None)
setattr(equipment['Roadm']['default'], equalization, target_value)
network = net_setup(equipment)
route = {
"route-object-include-exclude": [
{
"explicit-route-usage": "route-include-ero",
"index": 0,
"num-unnum-hop": {
"node-id": "trx Brest_KLA",
"link-tp-id": "link-tp-id is not used",
"hop-type": "STRICT"
}
},
{
"explicit-route-usage": "route-include-ero",
"index": 1,
"num-unnum-hop": {
"node-id": "trx Vannes_KBE",
"link-tp-id": "link-tp-id is not used",
"hop-type": "STRICT"
}
}
]
}
json_data = {
"path-request": [{
"request-id": "imposed_mode",
"source": "trx Brest_KLA",
"destination": "trx Vannes_KBE",
"src-tp-id": "trx Brest_KLA",
"dst-tp-id": "trx Vannes_KBE",
"bidirectional": False,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "test_offset",
"trx_mode": "mode 3",
"spacing": 75000000000.0,
"path_bandwidth": 100000000000.0
}
},
"explicit-route-objects": route
}, {
"request-id": "free_mode",
"source": "trx Brest_KLA",
"destination": "trx Vannes_KBE",
"src-tp-id": "trx Brest_KLA",
"dst-tp-id": "trx Vannes_KBE",
"bidirectional": False,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "test_offset",
"spacing": 75000000000.0,
"path_bandwidth": 100000000000.0
}
},
"explicit-route-objects": route
}]}
imposed_req, free_req, = requests_from_json(json_data, equipment)
assert free_req.tsp_mode is None
path_expected = compute_constrained_path(network, imposed_req)
_ = propagate(path_expected, imposed_req, equipment)
path = compute_constrained_path(network, free_req)
_, mode = propagate_and_optimize_mode(path, free_req, equipment)
assert mode['format'] == expected_mode
assert_allclose(path_expected[-1].snr_01nm, path[-1].snr_01nm, rtol=1e-5)

View File

@@ -355,7 +355,8 @@ def test_excel_ila_constraints(source, destination, route_list, hoptype, expecte
'nb_channel': 0,
'power': 0,
'path_bandwidth': 0,
'effective_freq_slot': None
'effective_freq_slot': None,
'equalization_offset_db': 0
}
request = PathRequest(**params)

View File

@@ -288,7 +288,8 @@ def request_set():
'min_spacing': 37.5e9,
'nb_channel': None,
'power': 0,
'path_bandwidth': 800e9}
'path_bandwidth': 800e9,
'equalization_offset_db': 0}
def test_freq_slot_exist(setup, equipment, request_set):