diff --git a/gnpy/core/elements.py b/gnpy/core/elements.py index 15c438545..6abc53614 100644 --- a/gnpy/core/elements.py +++ b/gnpy/core/elements.py @@ -30,9 +30,10 @@ from gnpy.core.utils import lin2db, db2lin, arrange_frequencies, snr_sum, per_label_average, pretty_summary_print, \ watt2dbm, psd2powerdbm -from gnpy.core.parameters import RoadmParams, FusedParams, FiberParams, PumpParams, EdfaParams, EdfaOperational +from gnpy.core.parameters import RoadmParams, FusedParams, FiberParams, PumpParams, EdfaParams, EdfaOperational, \ + RoadmPath, RoadmImpairment from gnpy.core.science_utils import NliSolver, RamanSolver -from gnpy.core.info import SpectralInformation, ReferenceCarrier +from gnpy.core.info import SpectralInformation from gnpy.core.exceptions import NetworkTopologyError, SpectrumError, ParametersError @@ -259,6 +260,17 @@ def __init__(self, *args, params=None, **kwargs): self.per_degree_pch_psw = self.params.per_degree_pch_psw self.ref_pch_in_dbm = {} self.ref_carrier = None + # Define the nature of from-to internal connection: express-path, drop-path, add-path + # roadm_paths contains a list of RoadmPath object for each path crossing the ROADM + self.roadm_paths = [] + # roadm_path_impairments contains a dictionnary of impairments profiles corresponding to type_variety + # first listed add, drop an express constitute the default + self.roadm_path_impairments = self.params.roadm_path_impairments + # per degree definitions, in case some degrees have particular deviations with respect to default. + self.per_degree_impairments = {f'{i["from_degree"]}-{i["to_degree"]}': {"from_degree": i["from_degree"], + "to_degree": i["to_degree"], + "impairment_id": i["impairment_id"]} + for i in self.params.per_degree_impairments} @property def to_json(self): @@ -289,6 +301,9 @@ def to_json(self): to_json['params']['per_degree_psd_out_mWperGHz'] = self.per_degree_pch_psd if self.per_degree_pch_psw: to_json['params']['per_degree_psd_out_mWperSlotWidth'] = self.per_degree_pch_psw + if self.per_degree_impairments: + to_json['per_degree_impairments'] = list(self.per_degree_impairments.values()) + return to_json def __repr__(self): @@ -404,11 +419,68 @@ def propagate(self, spectral_info, degree, from_degree): delta_power = watt2dbm(input_power) - new_target spectral_info.apply_attenuation_db(delta_power) - spectral_info.pmd = sqrt(spectral_info.pmd ** 2 + self.params.pmd ** 2) - spectral_info.pdl = sqrt(spectral_info.pdl ** 2 + self.params.pdl ** 2) + spectral_info.pmd = sqrt(spectral_info.pmd ** 2 + + self.get_roadm_path(from_degree=from_degree, to_degree=degree).impairment.pmd ** 2) + spectral_info.pdl = sqrt(spectral_info.pdl ** 2 + + self.get_roadm_path(from_degree=from_degree, to_degree=degree).impairment.pdl ** 2) self.pch_out_dbm = watt2dbm(spectral_info.signal + spectral_info.nli + spectral_info.ase) self.propagated_labels = spectral_info.label + def set_roadm_paths(self, from_degree, to_degree, path_type, impairment_id=None): + """set internal path type: express, drop or add with corresponding impairment + + If no impairment id is defined, then use the first profile that matches the path_type in the + profile dictionnary. + """ + # initialize impairment with params.pmd, params.cd + # if more detailed parameters are available for the Roadm, the use them instead + roadm_global_impairment = {'roadm-pmd': self.params.pmd, + 'roadm-pdl': self.params.pdl} + if path_type in ['add', 'drop']: + # without detailed imparments, we assume that add OSNR contribution is the same as drop contribution + # add_drop_osnr_db = - 10log10(1/add_osnr + 1/drop_osnr) with add_osnr = drop_osnr + # = add_osnr_db + 10log10(2) + roadm_global_impairment['roadm-osnr'] = self.params.add_drop_osnr + lin2db(2) + impairment = RoadmImpairment(roadm_global_impairment) + + if impairment_id is None: + # get the first item in the type variety that matches the path_type + for path_impairment_id, path_impairment in self.roadm_path_impairments.items(): + if path_impairment.path_type == path_type: + impairment = path_impairment + impairment_id = path_impairment_id + break + # at this point, path_type is not part of roadm_path_impairment, impairment and impairment_id are None + else: + if impairment_id in self.roadm_path_impairments: + impairment = self.roadm_path_impairments[impairment_id] + else: + msg = f'ROADM {self.uid}: impairment profile id {impairment_id} is not defined in library' + raise NetworkTopologyError(msg) + # print(from_degree, to_degree, path_type) + self.roadm_paths.append(RoadmPath(from_degree=from_degree, to_degree=to_degree, path_type=path_type, + impairment_id=impairment_id, impairment=impairment)) + + def get_roadm_path(self, from_degree, to_degree): + """Get internal path type impairment""" + for roadm_path in self.roadm_paths: + if roadm_path.from_degree == from_degree and roadm_path.to_degree == to_degree: + return roadm_path + msg = f'Could not find from_degree-to_degree {from_degree}-{to_degree} path in ROADM {self.uid}' + raise NetworkTopologyError(msg) + + def get_per_degree_impairment_id(self, from_degree, to_degree): + """returns the id of the impairment if the degrees are in the per_degree tab""" + if f'{from_degree}-{to_degree}' in self.per_degree_impairments.keys(): + return self.per_degree_impairments[f'{from_degree}-{to_degree}']["impairment_id"] + return None + + def get_path_type_per_id(self, impairment_id): + """returns the path_type of the impairment if the is is defined""" + if impairment_id in self.roadm_path_impairments.keys(): + return self.roadm_path_impairments[impairment_id].path_type + return None + def __call__(self, spectral_info, degree, from_degree): self.propagate(spectral_info, degree=degree, from_degree=from_degree) return spectral_info diff --git a/gnpy/core/network.py b/gnpy/core/network.py index 2a0f0e80a..25ba8bd67 100644 --- a/gnpy/core/network.py +++ b/gnpy/core/network.py @@ -530,6 +530,62 @@ def set_fiber_input_power(network, fiber, equipment, pref_ch_db): fiber.ref_pch_in_dbm = pref_ch_db - loss +def set_roadm_internal_paths(roadm, network): + """Set ROADM path types (express, add, drop) + + Uses implicit guess if no information is set in ROADM + """ + next_oms = [n.uid for n in network.successors(roadm) if not isinstance(n, elements.Transceiver)] + previous_oms = [n.uid for n in network.predecessors(roadm) if not isinstance(n, elements.Transceiver)] + drop_port = [n.uid for n in network.successors(roadm) if isinstance(n, elements.Transceiver)] + add_port = [n.uid for n in network.predecessors(roadm) if isinstance(n, elements.Transceiver)] + + default_express = 'express' + default_add = 'add' + default_drop = 'drop' + # take user defined element impairment id if it exists + correct_from_degrees = [] + correct_add = [] + correct_to_degrees = [] + correct_drop = [] + for from_degree in previous_oms: + correct_from_degrees.append(from_degree) + for to_degree in next_oms: + correct_to_degrees.append(to_degree) + impairment_id = roadm.get_per_degree_impairment_id(from_degree, to_degree) + roadm.set_roadm_paths(from_degree=from_degree, to_degree=to_degree, path_type=default_express, + impairment_id=impairment_id) + for drop in drop_port: + correct_drop.append(drop) + impairment_id = roadm.get_per_degree_impairment_id(from_degree, drop) + path_type = roadm.get_path_type_per_id(impairment_id) + # a degree connected to a transceiver MUST be add or drop + # but a degree connected to something else could be an express, add or drop + # (for example case of external shelves) + if path_type and path_type != 'drop': + msg = f'Roadm {roadm.uid} path_type is defined as {path_type} but it should be drop' + raise NetworkTopologyError(msg) + roadm.set_roadm_paths(from_degree=from_degree, to_degree=drop, path_type=default_drop, + impairment_id=impairment_id) + for to_degree in next_oms: + for add in add_port: + correct_add.append(add) + impairment_id = roadm.get_per_degree_impairment_id(add, to_degree) + path_type = roadm.get_path_type_per_id(impairment_id) + if path_type and path_type != 'add': + msg = f'Roadm {roadm.uid} path_type is defined as {path_type} but it should be add' + raise NetworkTopologyError(msg) + roadm.set_roadm_paths(from_degree=add, to_degree=to_degree, path_type=default_add, + impairment_id=impairment_id) + # sanity check: raise an error if per_degree from or to degrees are not in the correct list + # raise an error if user defined path_type is not consistent with inferred path_type: + for item in roadm.per_degree_impairments.values(): + if item['from_degree'] not in correct_from_degrees + correct_add or \ + item['to_degree'] not in correct_to_degrees + correct_drop: + msg = f'Roadm {roadm.uid} has wrong from-to degree uid {item["from_degree"]} - {item["to_degree"]}' + raise NetworkTopologyError(msg) + + def add_roadm_booster(network, roadm): next_nodes = [n for n in network.successors(roadm) if not (isinstance(n, elements.Transceiver) or isinstance(n, elements.Fused) @@ -777,6 +833,7 @@ def build_network(network, equipment, pref_ch_db, pref_total_db, set_connector_l set_egress_amplifier(network, roadm, equipment, pref_ch_db, pref_total_db, verbose) for roadm in roadms: set_roadm_input_powers(network, roadm, equipment, pref_ch_db) + set_roadm_internal_paths(roadm, network) for fiber in [f for f in network.nodes() if isinstance(f, (elements.Fiber, elements.RamanFiber))]: set_fiber_input_power(network, fiber, equipment, pref_ch_db) diff --git a/gnpy/core/parameters.py b/gnpy/core/parameters.py index 76735c8e9..b8afc6b54 100644 --- a/gnpy/core/parameters.py +++ b/gnpy/core/parameters.py @@ -113,8 +113,68 @@ def __init__(self, **kwargs): self.pmd = kwargs['pmd'] self.pdl = kwargs['pdl'] self.restrictions = kwargs['restrictions'] + self.roadm_path_impairments = self.get_roadm_path_impairments(kwargs['roadm-path-impairments']) except KeyError as e: raise ParametersError(f'ROADM configurations must include {e}. Configuration: {kwargs}') + self.per_degree_impairments = kwargs.get('per_degree_impairments', []) + + def get_roadm_path_impairments(self, path_impairments_list): + """Get the ROADM list of profiles for impairments definition + + transform the ietf model into gnpy internal model: add a path-type in the attributes + """ + if not path_impairments_list: + return {} + authorized_path_types = { + 'roadm-express-path': 'express', + 'roadm-add-path': 'add', + 'roadm-drop-path': 'drop', + } + roadm_path_impairments = {} + for path_impairment in path_impairments_list: + index = path_impairment['roadm-path-impairments-id'] + path_type = next(key for key in path_impairment if key in authorized_path_types.keys()) + impairment_dict = dict({'path-type': authorized_path_types[path_type]}, **path_impairment[path_type]) + roadm_path_impairments[index] = RoadmImpairment(impairment_dict) + return roadm_path_impairments + + +class RoadmPath: + def __init__(self, from_degree, to_degree, path_type, impairment_id=None, impairment=None): + """Records roadm internal paths, types and impairment + + path_type must be in "express", "add", "drop" + impairment_id must be one of the id detailed in equipement + """ + self.from_degree = from_degree + self.to_degree = to_degree + self.path_type = path_type + self.impairment_id = impairment_id + self.impairment = impairment + + +class RoadmImpairment: + """Generic definition of impairments for express, add and drop""" + def __init__(self, params): + """Records roadm internal paths and types""" + self.path_type = params.get('path-type') + self.pmd = params.get('roadm-pmd') + self.cd = params.get('roadm-cd') + self.pdl = params.get('roadm-pdl') + self.inband_crosstalk = params.get('roadm-inband-crosstalk') + self.maxloss = params.get('roadm-maxloss', 0) + if params.get('frequency-range') is not None: + self.fmin = params.get('frequency-range')['lower-frequency'] + self.fmax = params.get('frequency-range')['upper-frequency'] + else: + self.fmin, self.fmax = None, None + self.osnr = params.get('roadm-osnr', None) + self.pmax = params.get('roadm-pmax', None) + self.nf = params.get('roadm-noise-figure', None) + self.minloss = params.get('minloss', None) + self.typloss = params.get('typloss', None) + self.pmin = params.get('pmin', None) + self.ptyp = params.get('ptyp', None) class FusedParams(Parameters): diff --git a/gnpy/example-data/eqpt_config.json b/gnpy/example-data/eqpt_config.json index 556816188..bfc3ddf27 100644 --- a/gnpy/example-data/eqpt_config.json +++ b/gnpy/example-data/eqpt_config.json @@ -217,7 +217,70 @@ "restrictions": { "preamp_variety_list": [], "booster_variety_list": [] - } + }, + "roadm-path-impairments": [] + }, { + "type_variety": "detailed_impairments", + "target_pch_out_db": -20, + "add_drop_osnr": 38, + "pmd": 0, + "pdl": 0, + "restrictions": { + "preamp_variety_list":[], + "booster_variety_list":[] + }, + "roadm-path-impairments": [ + { + "roadm-path-impairments-id": 0, + "roadm-express-path": { + "frequency-range": { + "lower-frequency": 191.3e12, + "upper-frequency": 196.1e12 + }, + "roadm-pmd": 0, + "roadm-cd": 0, + "roadm-pdl": 0, + "roadm-inband-crosstalk": 0, + "roadm-maxloss": 16.5 + } + }, { + "roadm-path-impairments-id": 1, + "roadm-add-path": { + "frequency-range": { + "lower-frequency": 191.3e12, + "upper-frequency": 196.1e12 + }, + "roadm-pmd": 0, + "roadm-cd": 0, + "roadm-pdl": 0, + "roadm-inband-crosstalk": 0, + "roadm-maxloss": 11.5, + "roadm-pmax": 2.5, + "roadm-osnr": 41, + "roadm-noise-figure": 23 + } + }, { + "roadm-path-impairments-id": 2, + "roadm-drop-path": { + "frequency-range": { + "lower-frequency": 191.3e12, + "upper-frequency": 196.1e12 + }, + "roadm-pmd": 0, + "roadm-cd": 0, + "roadm-pdl": 0, + "roadm-inband-crosstalk": 0, + "roadm-maxloss": 11.5, + "roadm-minloss": 7.5, + "roadm-typloss": 10, + "roadm-pmin": -13.5, + "roadm-pmax": -9.5, + "roadm-ptyp": -12, + "roadm-osnr": 41, + "roadm-noise-figure": 15 + } + } + ] } ], "SI": [{ diff --git a/gnpy/tools/json_io.py b/gnpy/tools/json_io.py index ce8cc5311..834705582 100644 --- a/gnpy/tools/json_io.py +++ b/gnpy/tools/json_io.py @@ -102,7 +102,8 @@ class Roadm(_JsonThing): 'restrictions': { 'preamp_variety_list': [], 'booster_variety_list': [] - } + }, + 'roadm-path-impairments': [] } def __init__(self, **kwargs): diff --git a/tests/data/eqpt_config.json b/tests/data/eqpt_config.json index 414b8db61..aaff78ac7 100644 --- a/tests/data/eqpt_config.json +++ b/tests/data/eqpt_config.json @@ -86,7 +86,69 @@ "restrictions": { "preamp_variety_list": [], "booster_variety_list": [] - } + }, + "roadm-path-impairments": [] + }, { + "type_variety": "example_detailed_impairments", + "target_pch_out_db": -20, + "add_drop_osnr": 35, + "pmd": 0, + "pdl": 0, + "restrictions": { + "preamp_variety_list":[], + "booster_variety_list":[] + }, + "roadm-path-impairments": [ + { + "roadm-path-impairments-id": 0, + "roadm-express-path": { + "frequency-range": { + "lower-frequency": 191.3e12, + "upper-frequency": 196.1e12 + }, + "roadm-pmd": 0, + "roadm-cd": 0, + "roadm-pdl": 0, + "roadm-inband-crosstalk": 0, + "roadm-maxloss": 16.5 + } + }, { + "roadm-path-impairments-id": 1, + "roadm-add-path": { + "frequency-range": { + "lower-frequency": 191.3e12, + "upper-frequency": 196.1e12 + }, + "roadm-pmd": 0, + "roadm-cd": 0, + "roadm-pdl": 0, + "roadm-inband-crosstalk": 0, + "roadm-maxloss": 11.5, + "roadm-pmax": 2.5, + "roadm-osnr": 41, + "roadm-noise-figure": 23 + } + }, { + "roadm-path-impairments-id": 2, + "roadm-drop-path": { + "frequency-range": { + "lower-frequency": 191.3e12, + "upper-frequency": 196.1e12 + }, + "roadm-pmd": 0, + "roadm-cd": 0, + "roadm-pdl": 0, + "roadm-inband-crosstalk": 0, + "roadm-maxloss": 11.5, + "roadm-minloss": 7.5, + "roadm-typloss": 10, + "roadm-pmin": -13.5, + "roadm-pmax": -9.5, + "roadm-ptyp": -12, + "roadm-osnr": 41, + "roadm-noise-figure": 15 + } + }] }, { "target_pch_out_db": -20, "add_drop_osnr": 38, diff --git a/tests/invocation/logs_path_request b/tests/invocation/logs_path_request index 3db28623b..91a218199 100644 --- a/tests/invocation/logs_path_request +++ b/tests/invocation/logs_path_request @@ -2,6 +2,9 @@ INFO gnpy.tools.cli_examples:cli_examples.py Computing path requests meshTop WARNING gnpy.tools.json_io:json_io.py WARNING missing type_variety attribute in eqpt_config.json[Roadm] default value is type_variety = default +WARNING gnpy.tools.json_io:json_io.py + WARNING missing roadm-path-impairments attribute in eqpt_config.json[Roadm] + default value is roadm-path-impairments = [] INFO gnpy.tools.json_io:json_io.py Automatically converting requests from XLS to JSON INFO gnpy.topology.request:request.py request 0 diff --git a/tests/invocation/logs_path_requests_run_CD_PMD_PDL_missing b/tests/invocation/logs_path_requests_run_CD_PMD_PDL_missing index 24b3a8fef..d74d34b92 100644 --- a/tests/invocation/logs_path_requests_run_CD_PMD_PDL_missing +++ b/tests/invocation/logs_path_requests_run_CD_PMD_PDL_missing @@ -2,6 +2,9 @@ INFO gnpy.tools.cli_examples:cli_examples.py Computing path requests CORONET WARNING gnpy.tools.json_io:json_io.py WARNING missing type_variety attribute in eqpt_config.json[Roadm] default value is type_variety = default +WARNING gnpy.tools.json_io:json_io.py + WARNING missing roadm-path-impairments attribute in eqpt_config.json[Roadm] + default value is roadm-path-impairments = [] INFO gnpy.topology.request:request.py request 0 Computing path from trx Abilene to trx Albany diff --git a/tests/invocation/logs_power_sweep_example b/tests/invocation/logs_power_sweep_example index b94957400..a56a9c6e1 100644 --- a/tests/invocation/logs_power_sweep_example +++ b/tests/invocation/logs_power_sweep_example @@ -1,6 +1,9 @@ WARNING gnpy.tools.json_io:json_io.py WARNING missing type_variety attribute in eqpt_config.json[Roadm] default value is type_variety = default +WARNING gnpy.tools.json_io:json_io.py + WARNING missing roadm-path-impairments attribute in eqpt_config.json[Roadm] + default value is roadm-path-impairments = [] INFO gnpy.tools.cli_examples:cli_examples.py source = 'brest' INFO gnpy.tools.cli_examples:cli_examples.py destination = 'rennes' WARNING gnpy.core.network:network.py diff --git a/tests/invocation/logs_transmission_saturated b/tests/invocation/logs_transmission_saturated index 6b2e42876..07f93ef59 100644 --- a/tests/invocation/logs_transmission_saturated +++ b/tests/invocation/logs_transmission_saturated @@ -1,6 +1,9 @@ WARNING gnpy.tools.json_io:json_io.py WARNING missing type_variety attribute in eqpt_config.json[Roadm] default value is type_variety = default +WARNING gnpy.tools.json_io:json_io.py + WARNING missing roadm-path-impairments attribute in eqpt_config.json[Roadm] + default value is roadm-path-impairments = [] INFO gnpy.tools.cli_examples:cli_examples.py source = 'lannion' INFO gnpy.tools.cli_examples:cli_examples.py destination = 'lorient' WARNING gnpy.core.network:network.py diff --git a/tests/test_equalization.py b/tests/test_equalization.py index 9f76fe3dd..89606b047 100644 --- a/tests/test_equalization.py +++ b/tests/test_equalization.py @@ -69,10 +69,12 @@ def test_equalization_combination_degree(delta_pdb_per_channel, degree, equaliza "restrictions": { "preamp_variety_list": [], "booster_variety_list": [] - } + }, + "roadm-path-impairments": [] } } roadm = Roadm(**roadm_config) + roadm.set_roadm_paths(from_degree='tata', to_degree=degree, path_type='express') roadm.ref_pch_in_dbm['tata'] = 0 roadm.ref_carrier = ReferenceCarrier(baud_rate=32e9, slot_width=50e9) frequency = 191e12 + array([0, 50e9, 150e9, 225e9, 275e9]) @@ -231,7 +233,8 @@ def test_low_input_power(target_out, delta_pdb_per_channel, correction): "restrictions": { "preamp_variety_list": [], "booster_variety_list": [] - } + }, + "roadm-path-impairments": [] }, "metadata": { "location": { @@ -243,6 +246,7 @@ def test_low_input_power(target_out, delta_pdb_per_channel, correction): } } roadm = Roadm(**roadm_config) + roadm.set_roadm_paths(from_degree='tata', to_degree='toto', path_type='express') roadm.ref_pch_in_dbm['tata'] = 0 roadm.ref_carrier = ReferenceCarrier(baud_rate=32e9, slot_width=50e9) si = roadm(si, degree='toto', from_degree='tata') @@ -283,7 +287,8 @@ def test_2low_input_power(target_out, delta_pdb_per_channel, correction): "restrictions": { "preamp_variety_list": [], "booster_variety_list": [] - } + }, + "roadm-path-impairments": [] }, "metadata": { "location": { @@ -295,6 +300,7 @@ def test_2low_input_power(target_out, delta_pdb_per_channel, correction): } } roadm = Roadm(**roadm_config) + roadm.set_roadm_paths(from_degree='tata', to_degree='toto', path_type='express') roadm.ref_pch_in_dbm['tata'] = 0 roadm.ref_carrier = ReferenceCarrier(baud_rate=32e9, slot_width=50e9) si = roadm(si, degree='toto', from_degree='tata') diff --git a/tests/test_roadm_restrictions.py b/tests/test_roadm_restrictions.py index a37f3cbaf..18dab8682 100644 --- a/tests/test_roadm_restrictions.py +++ b/tests/test_roadm_restrictions.py @@ -23,8 +23,8 @@ from gnpy.core.equipment import trx_mode_params from gnpy.topology.request import PathRequest, compute_constrained_path, propagate from gnpy.core.info import create_input_spectral_information, Carrier -from gnpy.core.utils import db2lin, dbm2watt -from gnpy.core.exceptions import ConfigurationError +from gnpy.core.utils import db2lin, dbm2watt, merge_amplifier_restrictions +from gnpy.core.exceptions import ConfigurationError, NetworkTopologyError TEST_DIR = Path(__file__).parent @@ -549,3 +549,141 @@ def test_wrong_restrictions(restrictions, fail): else: equipment = _equipment_from_json(json_data, EQPT_LIBRARY_NAME) assert equipment['Roadm']['example_test'].restrictions == restrictions + + +@pytest.mark.parametrize('roadm, from_degree, to_degree, expected_impairment_id, expected_type', [ + ('roadm Lannion_CAS', 'trx Lannion_CAS', 'east edfa in Lannion_CAS to Corlay', 1, 'add'), + ('roadm Lannion_CAS', 'west edfa in Lannion_CAS to Stbrieuc', 'east edfa in Lannion_CAS to Corlay', 0, 'express'), + ('roadm Lannion_CAS', 'west edfa in Lannion_CAS to Stbrieuc', 'trx Lannion_CAS', 2, 'drop'), + ('roadm h', 'west edfa in h to g', 'trx h', None, 'drop') +]) +def test_roadm_impairments(roadm, from_degree, to_degree, expected_impairment_id, expected_type): + """Check that impairment id and types are correct + """ + json_data = load_json(NETWORK_FILE_NAME) + for el in json_data['elements']: + if el['uid'] == 'roadm Lannion_CAS': + el['type_variety'] = 'example_detailed_impairments' + equipment = load_equipment(EQPT_LIBRARY_NAME) + network = network_from_json(json_data, equipment) + build_network(network, equipment, 0.0, 20.0) + roadm = next(n for n in network.nodes() if n.uid == roadm) + assert roadm.get_roadm_path(from_degree, to_degree).path_type == expected_type + assert roadm.get_roadm_path(from_degree, to_degree).impairment_id == expected_impairment_id + + +@pytest.mark.parametrize('type_variety, from_degree, to_degree, impairment_id, expected_type', [ + (None, 'trx Lannion_CAS', 'east edfa in Lannion_CAS to Corlay', 1, 'add'), + ('default', 'trx Lannion_CAS', 'east edfa in Lannion_CAS to Corlay', 3, 'add'), + (None, 'west edfa in Lannion_CAS to Stbrieuc', 'east edfa in Lannion_CAS to Corlay', None, 'express') +]) +def test_roadm_per_degree_impairments(type_variety, from_degree, to_degree, impairment_id, expected_type): + """Check that impairment type is correct also if per degree impairment is defined + """ + json_data = load_json(EQPT_LIBRARY_NAME) + assert 'type_variety' not in json_data['Roadm'][2] + json_data['Roadm'][2]['roadm-path-impairments'] = [ + { + "roadm-path-impairments-id": 1, + "roadm-add-path": { + "roadm-osnr": 41, + } + }, { + "roadm-path-impairments-id": 3, + "roadm-add-path": { + "roadm-inband-crosstalk": 0, + "roadm-osnr": 20, + "roadm-noise-figure": 23 + } + }] + equipment = _equipment_from_json(json_data, EQPT_LIBRARY_NAME) + assert equipment['Roadm']['default'].type_variety == 'default' + + json_data = load_json(NETWORK_FILE_NAME) + for el in json_data['elements']: + if el['uid'] == 'roadm Lannion_CAS' and type_variety is not None: + el['type_variety'] = type_variety + el['params'] = { + "per_degree_impairments": [ + { + "from_degree": from_degree, + "to_degree": to_degree, + "impairment_id": impairment_id + }] + } + network = network_from_json(json_data, equipment) + build_network(network, equipment, 0.0, 20.0) + roadm = next(n for n in network.nodes() if n.uid == 'roadm Lannion_CAS') + assert roadm.get_roadm_path(from_degree, to_degree).path_type == expected_type + assert roadm.get_roadm_path(from_degree, to_degree).impairment_id == impairment_id + + +@pytest.mark.parametrize('from_degree, to_degree, impairment_id, error, message', [ + ('trx Lannion_CAS', 'east edfa in Lannion_CAS to Corlay', 2, NetworkTopologyError, + 'Roadm roadm Lannion_CAS path_type is defined as drop but it should be add'), # wrong path_type + ('trx Lannion_CAS', 'east edfa toto', 1, ConfigurationError, + 'Roadm roadm Lannion_CAS has wrong from-to degree uid trx Lannion_CAS - east edfa toto'), # wrong degree + ('trx Lannion_CAS', 'east edfa in Lannion_CAS to Corlay', 11, NetworkTopologyError, + 'ROADM roadm Lannion_CAS: impairment profile id 11 is not defined in library') # wrong impairment_id +]) +def test_wrong_roadm_per_degree_impairments(from_degree, to_degree, impairment_id, error, message): + """Check that wrong per degree definitions are correctly catched + """ + equipment = load_equipment(EQPT_LIBRARY_NAME) + json_data = load_json(NETWORK_FILE_NAME) + for el in json_data['elements']: + if el['uid'] == 'roadm Lannion_CAS': + el['type_variety'] = 'example_detailed_impairments' + el['params'] = { + "per_degree_impairments": [ + { + "from_degree": from_degree, + "to_degree": to_degree, + "impairment_id": impairment_id + }] + } + network = network_from_json(json_data, equipment) + with pytest.raises(error, match=message): + build_network(network, equipment, 0.0, 20.0) + + +@pytest.mark.parametrize('path_type, type_variety, expected_pmd, expected_pdl, expected_osnr', [ + ('express', 'default', 5.0e-12, 0.5, None), # roadm instance parameters pre-empts library + ('express', 'example_test', 5.0e-12, 0.5, None), + ('express', 'example_detailed_impairments', 0, 0, None), # detailed parameters pre-empts global instance ones + ('add', 'default', 5.0e-12, 0.5, None), + ('add', 'example_test', 5.0e-12, 0.5, None), + ('add', 'example_detailed_impairments', 0, 0, 41)]) +def test_impairment_initialization(path_type, type_variety, expected_pmd, expected_pdl, expected_osnr): + """Check that impairments are correctly initialized, with this order: + - use equipment roadm impairments if no impairment are set in the ROADM instance + - use roadm global impairment if roadm global impairment are set + - use roadm detailed impairment for the corresponding path_type if roadm type_variety has detailed impairments + - use roadm per degree impairment if they are defined + """ + equipment = load_equipment(EQPT_LIBRARY_NAME) + extra_params = equipment['Roadm'][type_variety].__dict__ + roadm_config = { + "uid": "roadm Lannion_CAS", + "params": { + "add_drop_osnr": 38, + "pmd": 5.0e-12, + "pdl": 0.5 + } + } + if type_variety != 'default': + roadm_config["type_variety"] = type_variety + roadm_config['params'] = merge_amplifier_restrictions(roadm_config['params'], extra_params) + roadm = Roadm(**roadm_config) + roadm.set_roadm_paths(from_degree='tata', to_degree='toto', path_type=path_type) + assert roadm.get_roadm_path(from_degree='tata', to_degree='toto').path_type == path_type + assert roadm.get_roadm_path(from_degree='tata', to_degree='toto').impairment.pmd == expected_pmd + assert roadm.get_roadm_path(from_degree='tata', to_degree='toto').impairment.pdl == expected_pdl + if path_type == 'add': + # we assume for simplicity that add contribution is the same as drop contribution + # add_drop_osnr_db = 10log10(1/add_osnr + 1/drop_osnr) + if type_variety in ['default', 'example_test']: + assert roadm.get_roadm_path(from_degree='tata', + to_degree='toto').impairment.osnr == roadm.params.add_drop_osnr + lin2db(2) + else: + assert roadm.get_roadm_path(from_degree='tata', to_degree='toto').impairment.osnr == expected_osnr