From a3edb2014238cb1b91a36db94b42a5261ec7f16f Mon Sep 17 00:00:00 2001 From: EstherLerouzic Date: Fri, 28 Oct 2022 09:02:44 +0200 Subject: [PATCH] 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 Change-Id: I437f75c42f257b88b24207260ef9ec9b1ab7066e --- gnpy/core/equipment.py | 5 +- gnpy/core/info.py | 5 +- gnpy/tools/cli_examples.py | 1 - gnpy/tools/json_io.py | 2 +- gnpy/topology/request.py | 30 ++-- tests/data/eqpt_config.json | 2 +- tests/test_equalization.py | 256 +++++++++++++++++++++++++++++- tests/test_parser.py | 3 +- tests/test_spectrum_assignment.py | 3 +- 9 files changed, 284 insertions(+), 23 deletions(-) diff --git a/gnpy/core/equipment.py b/gnpy/core/equipment.py index d69e0d8e1..50bb20b85 100644 --- a/gnpy/core/equipment.py +++ b/gnpy/core/equipment.py @@ -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 diff --git a/gnpy/core/info.py b/gnpy/core/info.py index 8046c24ad..e0667b97c 100644 --- a/gnpy/core/info.py +++ b/gnpy/core/info.py @@ -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, diff --git a/gnpy/tools/cli_examples.py b/gnpy/tools/cli_examples.py index ba0e22665..ee0d5f93a 100644 --- a/gnpy/tools/cli_examples.py +++ b/gnpy/tools/cli_examples.py @@ -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: diff --git a/gnpy/tools/json_io.py b/gnpy/tools/json_io.py index 7abd97954..a07376ccd 100644 --- a/gnpy/tools/json_io.py +++ b/gnpy/tools/json_io.py @@ -133,6 +133,7 @@ def __init__(self, **kwargs): 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: diff --git a/gnpy/topology/request.py b/gnpy/topology/request.py index 10a9b6ed8..ab303755a 100644 --- a/gnpy/topology/request.py +++ b/gnpy/topology/request.py @@ -35,14 +35,14 @@ 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 @@ def __init__(self, *args, **params): 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): diff --git a/tests/data/eqpt_config.json b/tests/data/eqpt_config.json index 8dbb5b895..8585c12e2 100644 --- a/tests/data/eqpt_config.json +++ b/tests/data/eqpt_config.json @@ -217,7 +217,7 @@ "tx_osnr": 45, "min_spacing": 75e9, "cost":1 - }, + }, { "format": "mode 4", "baud_rate": 66e9, diff --git a/tests/test_equalization.py b/tests/test_equalization.py index a7275372d..4e8061b86 100644 --- a/tests/test_equalization.py +++ b/tests/test_equalization.py @@ -12,6 +12,7 @@ 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 @@ 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) diff --git a/tests/test_parser.py b/tests/test_parser.py index b80d2fa9b..02d41c0e1 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -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) diff --git a/tests/test_spectrum_assignment.py b/tests/test_spectrum_assignment.py index e017bdef2..60c524b49 100644 --- a/tests/test_spectrum_assignment.py +++ b/tests/test_spectrum_assignment.py @@ -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):