diff --git a/gnpy/core/elements.py b/gnpy/core/elements.py index cf0f91584..5b4fdd1df 100644 --- a/gnpy/core/elements.py +++ b/gnpy/core/elements.py @@ -25,16 +25,16 @@ from scipy.constants import h, c from scipy.interpolate import interp1d from collections import namedtuple -from typing import Union +from typing import Union, List from logging import getLogger import warnings from gnpy.core.utils import lin2db, db2lin, arrange_frequencies, snr_sum, per_label_average, pretty_summary_print, \ - watt2dbm, psd2powerdbm, calculate_absolute_min_or_zero + watt2dbm, psd2powerdbm, calculate_absolute_min_or_zero, nice_column_str from gnpy.core.parameters import RoadmParams, FusedParams, FiberParams, PumpParams, EdfaParams, EdfaOperational, \ - RoadmPath, RoadmImpairment + MultiBandParams, RoadmPath, RoadmImpairment, find_band_name, FrequencyBand from gnpy.core.science_utils import NliSolver, RamanSolver -from gnpy.core.info import SpectralInformation, demuxed_spectral_information +from gnpy.core.info import SpectralInformation, muxed_spectral_information, demuxed_spectral_information from gnpy.core.exceptions import NetworkTopologyError, SpectrumError, ParametersError @@ -1211,3 +1211,127 @@ def __call__(self, spectral_info): self.propagate(spectral_info) return spectral_info raise ValueError(f'Amp {self.uid} Defined propagation band does not match amplifiers band.') + + +class Multiband_amplifier(_Node): + """Represents a multiband amplifier that manages multiple amplifiers across different frequency bands. + + This class allows for the initialization and management of amplifiers, each associated with a specific + frequency band. It provides methods for signal propagation through the amplifiers and for exporting + to JSON format. + + Parameters: + ----------- + amplifiers : list of dict + A list of dictionaries, each containing parameters for setting an individual amplifier. + params : dict + A dictionary of parameters for the multiband amplifier, which must include necessary configuration + settings. + *args, **kwargs : + Additional positional and keyword arguments passed to the parent class `_Node`. + + Attributes: + ----------- + variety_list : list + A list of varieties associated with the amplifier. + amplifiers : dict + A dictionary mapping band names to their corresponding amplifier instances. + + Methods: + -------- + __call__(spectral_info): + Propagates the input spectral information through each amplifier and returns the multiplexed spectrum. + + to_json: + Converts the amplifier's state to a JSON-compatible dictionary. + + __repr__(): + Returns a string representation of the multiband amplifier instance. + + __str__(): + Returns a formatted string representation of the multiband amplifier and its amplifiers. + + Raises: + ------- + ParametersError: + If there are conflicting amplifier definitions for the same frequency band during initialization. + + ValueError: + If the input spectral information does not match any defined amplifier bands during propagation. + """ + # separate the top level type_variety from kwargs to avoid having multiple type_varieties on each element processing + def __init__(self, *args, amplifiers: List[dict], params: dict, **kwargs): + self.variety_list = kwargs.pop('variety_list', None) + try: + super().__init__(params=MultiBandParams(**params), **kwargs) + except ParametersError as e: + raise ParametersError(f'{kwargs["uid"]}: {e}') + self.amplifiers = {} + if 'type_variety' in kwargs: + kwargs.pop('type_variety') + self.passive = False + for amp_dict in amplifiers: + # amplifiers dict uses default names as key to represent the band + amp = Edfa(**amp_dict, **kwargs) + band = next(b for b in amp.params.bands) + band_name = find_band_name(FrequencyBand(f_min=band["f_min"], f_max=band["f_max"])) + if band_name not in self.amplifiers.keys() and band not in self.params.bands: + self.params.bands.append(band) + self.amplifiers[band_name] = amp + elif band_name not in self.amplifiers.keys() and band in self.params.bands: + self.amplifiers[band_name] = amp + else: + raise ParametersError(f'{kwargs["uid"]}: has more than one amp defined for the same band') + + def __call__(self, spectral_info: SpectralInformation): + """propagates in each amp and returns the muxed spectrum + """ + out_si = [] + for _, amp in self.amplifiers.items(): + si = demuxed_spectral_information(spectral_info, amp.params.bands[0]) + # if spectral_info frequencies are outside amp band, si is None + if si: + si = amp(si) + out_si.append(si) + if not out_si: + raise ValueError('Defined propagation band does not match amplifiers band.') + return muxed_spectral_information(out_si) + + @property + def to_json(self): + return {'uid': self.uid, + 'type': type(self).__name__, + 'type_variety': self.type_variety, + 'amplifiers': [{ + 'type_variety': amp.type_variety, + 'operational': { + 'gain_target': round(amp.effective_gain, 6), + 'delta_p': amp.delta_p, + 'tilt_target': amp.tilt_target, + 'out_voa': amp.out_voa + }} for amp in self.amplifiers.values() + ], + 'metadata': { + 'location': self.metadata['location']._asdict() + } + } + + def __repr__(self): + return (f'{type(self).__name__}(uid={self.uid!r}, ' + f'type_variety={self.type_variety!r}, ') + + def __str__(self): + amp_str = [f'{type(self).__name__} {self.uid}', + f' type_variety: {self.type_variety}'] + multi_str_data = [] + max_width = 0 + for amp in self.amplifiers.values(): + lines = amp.__str__().split('\n') + # start at index 1 to remove uid from each amp list of strings + # records only if amp is used ie si has frequencies in amp) otherwise there is no other string than the uid + if len(lines) > 1: + max_width = max(max_width, max([len(line) for line in lines[1:]])) + multi_str_data.append(lines[1:]) + # multi_str_data contains lines with each amp str, instead we want to print per column: transpose the string + transposed_data = list(map(list, zip(*multi_str_data))) + return '\n'.join(amp_str) + '\n' + nice_column_str(data=transposed_data, max_length=max_width + 2, padding=3) diff --git a/gnpy/core/equipment.py b/gnpy/core/equipment.py index f20df7e00..fdf62f9b6 100644 --- a/gnpy/core/equipment.py +++ b/gnpy/core/equipment.py @@ -7,8 +7,11 @@ This module contains functionality for specifying equipment. """ +from collections import defaultdict +from functools import reduce +from typing import List -from gnpy.core.exceptions import EquipmentConfigError +from gnpy.core.exceptions import EquipmentConfigError, ConfigurationError def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=False): @@ -80,3 +83,50 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F trx_params = {**default_trx_params} return trx_params + + +def find_type_variety(amps: List[str], equipment: dict) -> str: + """Returns the multiband type_variety associated with a list of single band type_varieties + Args: + amps (List[str]): A list of single band type_varieties. + equipment (dict): A dictionary containing equipment information. + + Returns: + str: an amplifier type variety + """ + listes = find_type_varieties(amps, equipment) + + _found_type = list(reduce(lambda x, y: set(x) & set(y), listes)) + # Given a list of single band amplifiers, find the multiband amplifier whose multi_band group + # matches. For example, if amps list contains ["a1_LBAND", "a2_CBAND"], with a1.multi_band = [a1_LBAND, a1_CBAND] + # and a2.multi_band = [a1_LBAND, a2_CBAND], then: + # possible_type_varieties = {"a1_LBAND": ["a1", "a2"], "a2_CBAND": ["a2"]} + # listes = [["a1", "a2"], ["a2"]] + # and _found_type = [a2] + if not _found_type: + msg = f'{amps} amps do not belong to the same amp type {listes}' + raise ConfigurationError(msg) + return _found_type[0] + + +def find_type_varieties(amps: List[str], equipment: dict) -> List[List[str]]: + """Returns the multiband list of type_varieties associated with a list of single band type_varieties + Args: + amps (List[str]): A list of single band type_varieties. + equipment (dict): A dictionary containing equipment information. + + Returns: + List[List[str]]: A list of lists containing the multiband type_varieties + associated with each single band type_variety. + """ + possible_type_varieties = defaultdict(list) + for amp_name, amp in equipment['Edfa'].items(): + if amp.multi_band is not None: + for elem in amp.multi_band: + # possible_type_varieties stores the list of multiband amp names that list this elem as + # a possible amplifier of the multiband group. For example, if "std_medium_gain_multiband" + # and "std_medium_gain_multiband_new" contain "std_medium_gain_C" in their "multi_band" list, then: + # possible_type_varieties["std_medium_gain_C"] = + # ["std_medium_gain_multiband", "std_medium_gain_multiband_new"] + possible_type_varieties[elem].append(amp_name) + return [possible_type_varieties[a] for a in amps] diff --git a/gnpy/core/network.py b/gnpy/core/network.py index a242922bb..2b31de389 100644 --- a/gnpy/core/network.py +++ b/gnpy/core/network.py @@ -422,7 +422,7 @@ def set_egress_amplifier(network, this_node, equipment, pref_ch_db, pref_total_d restrictions = next_node.restrictions['preamp_variety_list'] else: restrictions = None - edfa_eqpt = {n: a for n, a in equipment['Edfa'].items()} + edfa_eqpt = {n: a for n, a in equipment['Edfa'].items() if a.type_def != 'multi_band'} edfa_variety, power_reduction = \ select_edfa(raman_allowed, gain_target, power_target, edfa_eqpt, node.uid, @@ -480,6 +480,35 @@ def set_egress_amplifier(network, this_node, equipment, pref_ch_db, pref_total_d node.target_pch_out_dbm = None elif isinstance(node, elements.RamanFiber): _ = span_loss(network, node, equipment, input_power=pref_ch_db + dp) + if isinstance(node, elements.Multiband_amplifier): + for amp in node.amplifiers.values(): + node_loss = span_loss(network, prev_node, equipment) + voa = amp.out_voa if amp.out_voa else 0 + if amp.delta_p is None: + dp = target_power(network, next_node, equipment) + voa + else: + dp = amp.delta_p + if amp.effective_gain is None or power_mode: + gain_target = node_loss + dp - prev_dp + prev_voa + else: # gain mode with effective_gain + gain_target = amp.effective_gain + dp = prev_dp - node_loss - prev_voa + gain_target + + power_target = pref_total_db + dp + amp.delta_p = dp if power_mode else None + amp.effective_gain = gain_target + set_amplifier_voa(amp, power_target, power_mode) + amp._delta_p = amp.delta_p if power_mode else dp + # target_pch_out_dbm records target power for design: If user defines one, then this is displayed, + # else display the one computed during design + if amp.delta_p is not None and amp.operational.delta_p is not None: + # use the user defined target + amp.target_pch_out_dbm = round(amp.operational.delta_p + pref_ch_db, 2) + elif amp.delta_p is not None: + # use the design target if no target were set + amp.target_pch_out_dbm = round(amp.delta_p + pref_ch_db, 2) + elif amp.delta_p is None: + amp.target_pch_out_dbm = None prev_dp = dp prev_voa = voa prev_node = node @@ -549,6 +578,10 @@ def set_roadm_input_powers(network, roadm, equipment, pref_ch_db): node.get_per_degree_ref_power(degree=previous_node.uid) - loss elif isinstance(node, elements.Transceiver): roadm.ref_pch_in_dbm[element.uid] = pref_ch_db - loss + elif isinstance(node, elements.Multiband_amplifier): + # use the worst (min) value among amps + roadm.ref_pch_in_dbm[element.uid] = min([pref_ch_db + amp._delta_p - amp.out_voa - loss + for amp in node.amplifiers.values()]) # check if target power can be met temp = [] if roadm.per_degree_pch_out_dbm: @@ -598,6 +631,9 @@ def set_fiber_input_power(network, fiber, equipment, pref_ch_db): fiber.ref_pch_in_dbm = pref_ch_db + node._delta_p - node.out_voa - loss elif isinstance(node, elements.Transceiver): fiber.ref_pch_in_dbm = pref_ch_db - loss + elif isinstance(node, elements.Multiband_amplifier): + # use the worst (min) value among amps + fiber.ref_pch_in_dbm = min([pref_ch_db + amp._delta_p - amp.out_voa - loss for amp in node.amplifiers.values()]) def set_roadm_internal_paths(roadm, network): @@ -658,7 +694,8 @@ def set_roadm_internal_paths(roadm, network): def add_roadm_booster(network, roadm): next_nodes = [n for n in network.successors(roadm) - if not isinstance(n, (elements.Transceiver, elements.Fused, elements.Edfa))] + if not isinstance(n, (elements.Transceiver, elements.Fused, elements.Edfa, + elements.Multiband_amplifier))] # no amplification for fused spans or TRX for next_node in next_nodes: network.remove_edge(roadm, next_node) @@ -684,7 +721,8 @@ def add_roadm_booster(network, roadm): def add_roadm_preamp(network, roadm): prev_nodes = [n for n in network.predecessors(roadm) - if not isinstance(n, (elements.Transceiver, elements.Fused, elements.Edfa))] + if not isinstance(n, (elements.Transceiver, elements.Fused, elements.Edfa, + elements.Multiband_amplifier))] # no amplification for fused spans or TRX for prev_node in prev_nodes: network.remove_edge(prev_node, roadm) diff --git a/gnpy/core/parameters.py b/gnpy/core/parameters.py index 2541241ed..85ef544cf 100644 --- a/gnpy/core/parameters.py +++ b/gnpy/core/parameters.py @@ -8,7 +8,8 @@ This module contains all parameters to configure standard network elements. """ from collections import namedtuple - +from copy import deepcopy +from dataclasses import dataclass from scipy.constants import c, pi from numpy import asarray, array, exp, sqrt, log, outer, ones, squeeze, append, flip, linspace, full @@ -593,7 +594,7 @@ def __init__(self, **params): def update_params(self, kwargs): for k, v in kwargs.items(): - setattr(self, k, self.update_params(**v) if isinstance(v, dict) else v) + setattr(self, k, v) class EdfaOperational: @@ -616,3 +617,56 @@ def __repr__(self): return (f'{type(self).__name__}(' f'gain_target={self.gain_target!r}, ' f'tilt_target={self.tilt_target!r})') + + +class MultiBandParams: + default_values = { + 'bands': [], + 'type_variety': '', + 'type_def': None, + 'allowed_for_design': False + } + + def __init__(self, **params): + try: + self.update_attr(params) + except KeyError as e: + raise ParametersError(f'Multiband configurations json must include {e}. Configuration: {params}') + + def update_attr(self, kwargs): + clean_kwargs = {k: v for k, v in kwargs.items() if v != ''} + for k, v in self.default_values.items(): + # use deepcopy to avoid sharing same object amongst all instance when v is a list or a dict! + if isinstance(v, (list, dict)): + setattr(self, k, clean_kwargs.get(k, deepcopy(v))) + else: + setattr(self, k, clean_kwargs.get(k, v)) + + +@dataclass +class FrequencyBand: + """Frequency band + """ + f_min: float + f_max: float + + +DEFAULT_BANDS_DEFINITION = { + "LBAND": FrequencyBand(f_min=187e12, f_max=189e12), + "CBAND": FrequencyBand(f_min=191.3e12, f_max=196.0e12) +} +# use this definition to index amplifiers'element of a multiband amplifier. +# this is not the design band + + +def find_band_name(band: FrequencyBand) -> str: + """return the default band name (CBAND, LBAND, ...) that corresponds to the band frequency range + Use the band center frequency: if center frequency is inside the band then returns CBAND. + This is to flexibly encompass all kind of bands definitions. + returns the first matching band name. + """ + for band_name, frequency_range in DEFAULT_BANDS_DEFINITION.items(): + center_frequency = (band.f_min + band.f_max) / 2 + if center_frequency >= frequency_range.f_min and center_frequency <= frequency_range.f_max: + return band_name + return 'unknown_band' diff --git a/gnpy/core/utils.py b/gnpy/core/utils.py index b8783e526..60c380b73 100644 --- a/gnpy/core/utils.py +++ b/gnpy/core/utils.py @@ -472,6 +472,26 @@ def calculate_absolute_min_or_zero(x: array) -> array: return (abs(x) - x) / 2 +def nice_column_str(data: List[List[str]], max_length: int = 30, padding: int = 1) -> str: + """data is a list of rows, creates strings with nice alignment per colum and padding with spaces + letf justified + + >>> table_data = [['aaa', 'b', 'c'], ['aaaaaaaa', 'bbb', 'c'], ['a', 'bbbbbbbbbb', 'c']] + >>> print(nice_column_str(table_data)) + aaa b c + aaaaaaaa bbb c + a bbbbbbbbbb c + """ + # transpose data to determine size of columns + transposed_data = list(map(list, zip(*data))) + column_width = [max(len(word) for word in column) + padding for column in transposed_data] + nice_str = [] + for row in data: + column = ''.join(word[0:max_length].ljust(min(width, max_length)) for width, word in zip(column_width, row)) + nice_str.append(f'{column}') + return '\n'.join(nice_str) + + def find_common_range(amp_bands: List[List[dict]], default_band_f_min: float, default_band_f_max: float) \ -> List[dict]: """Find the common frequency range of bands diff --git a/gnpy/tools/cli_examples.py b/gnpy/tools/cli_examples.py index 5cb284638..c923b4ce9 100644 --- a/gnpy/tools/cli_examples.py +++ b/gnpy/tools/cli_examples.py @@ -228,7 +228,7 @@ def transmission_main_example(args=None): print(f'Input optical power reference in span = {ansi_escapes.cyan}{power_dbm:.2f} ' + f'dBm{ansi_escapes.reset}:') else: - print('\nPropagating in gain mode: power cannot be set manually') + print('\nPropagating in {ansi_escapes.cyan}gain mode{ansi_escapes.reset}: power cannot be set manually') if len(powers_dbm) == 1: for elem in path: print(elem) diff --git a/gnpy/tools/json_io.py b/gnpy/tools/json_io.py index bd15f309e..b74c4df95 100644 --- a/gnpy/tools/json_io.py +++ b/gnpy/tools/json_io.py @@ -14,14 +14,15 @@ import json from collections import namedtuple from numpy import arange +from copy import deepcopy from gnpy.core import elements -from gnpy.core.equipment import trx_mode_params +from gnpy.core.equipment import trx_mode_params, find_type_variety from gnpy.core.exceptions import ConfigurationError, EquipmentConfigError, NetworkTopologyError, ServiceError from gnpy.core.science_utils import estimate_nf_model from gnpy.core.info import Carrier from gnpy.core.utils import automatic_nch, automatic_fmax, merge_amplifier_restrictions, dbm2watt -from gnpy.core.parameters import DEFAULT_RAMAN_COEFFICIENT, EdfaParams +from gnpy.core.parameters import DEFAULT_RAMAN_COEFFICIENT, EdfaParams, MultiBandParams from gnpy.topology.request import PathRequest, Disjunction, compute_spectrum_slot_vs_bandwidth from gnpy.topology.spectrum_assignment import mvalue_to_slots from gnpy.tools.convert import xls_to_json_data @@ -197,6 +198,7 @@ def from_json(cls, filename, **kwargs): type_def = kwargs.get('type_def', 'variable_gain') # default compatibility with older json eqpt files nf_def = None dual_stage_def = None + amplifiers = None if type_def == 'fixed_gain': try: @@ -241,16 +243,25 @@ def from_json(cls, filename, **kwargs): preamp_variety = kwargs.pop('preamp_variety') booster_variety = kwargs.pop('booster_variety') except KeyError: - msg = f'missing preamp/booster variety input for amplifier: {type_variety} in equipment config' - raise EquipmentConfigError(msg) + raise EquipmentConfigError(f'missing preamp/booster variety input for amplifier: {type_variety}' + + ' in equipment config') dual_stage_def = Model_dual_stage(preamp_variety, booster_variety) + elif type_def == 'multi_band': + amplifiers = kwargs['amplifiers'] else: raise EquipmentConfigError(f'Edfa type_def {type_def} does not exist') json_data = load_json(config) - + # raise an error if config does not contain f_min, f_max + if 'f_min' not in json_data or 'f_max' not in json_data: + raise EquipmentConfigError('default Edfa config does not contain f_min and f_max values.' + + ' Please correct file.') + # use f_min, f_max from kwargs + if 'f_min' in kwargs: + json_data.pop('f_min', None) + json_data.pop('f_max', None) return cls(**{**kwargs, **json_data, - 'nf_model': nf_def, 'dual_stage_model': dual_stage_def}) + 'nf_model': nf_def, 'dual_stage_model': dual_stage_def, 'multi_band': amplifiers}) def _automatic_spacing(baud_rate): @@ -490,6 +501,8 @@ def _cls_for(equipment_type): return elements.Fiber elif equipment_type == 'RamanFiber': return elements.RamanFiber + elif equipment_type == 'Multiband_amplifier': + return elements.Multiband_amplifier else: raise ConfigurationError(f'Unknown network equipment "{equipment_type}"') @@ -503,7 +516,53 @@ def network_from_json(json_data, equipment): typ = el_config.pop('type') variety = el_config.pop('type_variety', 'default') cls = _cls_for(typ) - if typ == 'Fused': + if typ == 'Multiband_amplifier': + if variety in ['default', '']: + extra_params = None + temp = el_config.setdefault('params', {}) + temp = merge_amplifier_restrictions(temp, deepcopy(MultiBandParams.default_values)) + el_config['params'] = temp + else: + extra_params = equipment['Edfa'][variety] + temp = el_config.setdefault('params', {}) + # use config params preferably to library params, only use library params to fill in + # the missing attribute + temp = merge_amplifier_restrictions(temp, deepcopy(extra_params.__dict__)) + el_config['params'] = temp + el_config['type_variety'] = variety + # if config does not contain any amp list create one + amps = el_config.setdefault('amplifiers', []) + for amp in amps: + amp_variety = amp['type_variety'] # juste pour essayer + amp_extra_params = equipment['Edfa'][amp_variety] + temp = amp.setdefault('params', {}) + temp = merge_amplifier_restrictions(temp, amp_extra_params.__dict__) + amp['params'] = temp + amp['type_variety'] = amp_variety + # check type_variety consistant with amps type_variety + if amps: + try: + multiband_type_variety = find_type_variety([a['type_variety'] for a in amps], equipment) + except ConfigurationError as e: + msg = f'Node {el_config["uid"]}: {e}' + raise ConfigurationError(msg) + if variety is not None and variety != multiband_type_variety: + raise ConfigurationError(f'In node {el_config["uid"]}: multiband amplifier type_variety is not ' + + 'consistent with its amps type varieties.') + if not amps and extra_params is not None: + # the amp config does not contain the amplifiers operational settings, but has a type_variety + # defined so that it is possible to create the template of amps for design for each band. This + # defines the default design bands. + # This lopp populates each amp with default values, for each band + for band in extra_params.bands: + params = {k: v for k, v in Amp.default_values.items()} + # update frequencies with band values + params['f_min'] = band['f_min'] + params['f_max'] = band['f_max'] + amps.append({'params': params}) + # without type_variety, it is not possible to set the amplifier dict at this point: need to wait + # for design, and use user defined design-bands + elif typ == 'Fused': # well, there's no variety for the 'Fused' node type pass elif variety in equipment[typ]: diff --git a/gnpy/topology/request.py b/gnpy/topology/request.py index 9d907e631..9478fa049 100644 --- a/gnpy/topology/request.py +++ b/gnpy/topology/request.py @@ -23,7 +23,7 @@ from networkx.utils import pairwise from numpy import mean, argmin -from gnpy.core.elements import Transceiver, Roadm, Edfa +from gnpy.core.elements import Transceiver, Roadm, Edfa, Multiband_amplifier from gnpy.core.utils import lin2db, find_common_range from gnpy.core.info import create_input_spectral_information, carriers_to_spectral_information, \ demuxed_spectral_information, muxed_spectral_information, SpectralInformation @@ -1255,5 +1255,5 @@ def find_elements_common_range(el_list: list, equipment: dict) -> List[dict]: """Find the common frequency range of amps of a given list of elements (for example an OMS or a path) If there are no amplifiers in the path, then use the SI """ - amp_bands = [n.params.bands for n in el_list if isinstance(n, (Edfa))] + amp_bands = [n.params.bands for n in el_list if isinstance(n, (Edfa, Multiband_amplifier))] return find_common_range(amp_bands, equipment['SI']['default'].f_min, equipment['SI']['default'].f_max) diff --git a/gnpy/topology/spectrum_assignment.py b/gnpy/topology/spectrum_assignment.py index 4caaeacaf..c789e3b56 100644 --- a/gnpy/topology/spectrum_assignment.py +++ b/gnpy/topology/spectrum_assignment.py @@ -15,7 +15,8 @@ from collections import namedtuple from logging import getLogger -from gnpy.core.elements import Roadm, Transceiver, Edfa + +from gnpy.core.elements import Roadm, Transceiver, Edfa, Multiband_amplifier from gnpy.core.exceptions import ServiceError, SpectrumError from gnpy.core.utils import order_slots, restore_order from gnpy.topology.request import compute_spectrum_slot_vs_bandwidth, find_elements_common_range @@ -230,7 +231,7 @@ def align_grids(oms_list): def find_network_freq_range(network, equipment): """Find the lowest freq from amps and highest freq among all amps to determine the resulting bitmap """ - amp_bands = [band for n in network.nodes() if isinstance(n, Edfa) for band in n.params.bands] + amp_bands = [band for n in network.nodes() if isinstance(n, (Edfa, Multiband_amplifier)) for band in n.params.bands] min_frequencies = [a['f_min'] for a in amp_bands] max_frequencies = [a['f_max'] for a in amp_bands] return min(min_frequencies), max(max_frequencies) diff --git a/tests/test_amplifier.py b/tests/test_amplifier.py index 8905d2175..0881eabee 100644 --- a/tests/test_amplifier.py +++ b/tests/test_amplifier.py @@ -9,7 +9,7 @@ from gnpy.core.utils import automatic_fmax, lin2db, db2lin, merge_amplifier_restrictions, dbm2watt, watt2dbm from gnpy.core.info import create_input_spectral_information, create_arbitrary_spectral_information from gnpy.core.network import build_network, set_amplifier_voa -from gnpy.tools.json_io import load_network, load_equipment, network_from_json +from gnpy.tools.json_io import load_network, load_equipment, load_json, _equipment_from_json, network_from_json from pathlib import Path import pytest @@ -365,3 +365,141 @@ def test_set_out_voa(): assert amp.out_voa == 4.0 assert amp.effective_gain == 20.0 + 4.0 assert amp.delta_p == -3.0 + 4.0 + + +def test_multiband(): + + equipment_json = load_json(eqpt_library) + # add some multiband amplifiers + amps = [ + { + "type_variety": "std_medium_gain_C", + "f_min": 191.25e12, + "f_max": 196.15e12, + "type_def": "variable_gain", + "gain_flatmax": 26, + "gain_min": 15, + "p_max": 21, + "nf_min": 6, + "nf_max": 10, + "out_voa_auto": False, + "allowed_for_design": True}, + { + "type_variety": "std_medium_gain_L", + "f_min": 186.55e12, + "f_max": 190.05e12, + "type_def": "variable_gain", + "gain_flatmax": 26, + "gain_min": 15, + "p_max": 21, + "nf_min": 6, + "nf_max": 10, + "out_voa_auto": False, + "allowed_for_design": True}, + { + "type_variety": "std_medium_gain_multiband", + "type_def": "multi_band", + "amplifiers": [ + "std_medium_gain_C", + "std_medium_gain_L" + ], + "allowed_for_design": False + } + ] + equipment_json['Edfa'].extend(amps) + + equipment = _equipment_from_json(equipment_json, eqpt_library) + + el_config = { + "uid": "Edfa1", + "type": "Multiband_amplifier", + "type_variety": "std_medium_gain_multiband", + "amplifiers": [ + { + "type_variety": "std_medium_gain_C", + "operational": { + "gain_target": 22.55, + "delta_p": 0.9, + "out_voa": 3.0, + "tilt_target": 0.0, + } + }, + { + "type_variety": "std_medium_gain_L", + "operational": { + "gain_target": 21, + "delta_p": 3.0, + "out_voa": 3.0, + "tilt_target": 0.0, + } + } + ] + } + fused_config = { + "uid": "[83/WR-2-4-SIG=>930/WRT-1-2-SIG]-Tl/9300", + "type": "Fused", + "params": { + "loss": 20 + } + } + json_data = { + "elements": [ + el_config, + fused_config + ], + "connections": [] + } + network = network_from_json(json_data, equipment) + amp = next(n for n in network.nodes() if n.uid == 'Edfa1') + fused = next(n for n in network.nodes() if n.uid == '[83/WR-2-4-SIG=>930/WRT-1-2-SIG]-Tl/9300') + si = create_input_spectral_information(f_min=186e12, f_max=196e12, roll_off=0.15, baud_rate=32e9, tx_power=1e-3, + spacing=50e9, tx_osnr=40.0) + assert si.number_of_channels == 200 + si = fused(si) + si = amp(si) + # assert nb of channel after mux/demux + assert si.number_of_channels == 164 # computed based on amp bands + # Check that multiband amp is correctly created with correct __str__ + actual_c_amp = amp.amplifiers["CBAND"].__str__() + expected_c_amp = '\n'.join([ + 'Edfa Edfa1', + ' type_variety: std_medium_gain_C', + ' effective gain(dB): 21.22', + ' (before att_in and before output VOA)', + ' noise figure (dB): 6.32', + ' (including att_in)', + ' pad att_in (dB): 0.00', + ' Power In (dBm): -0.22', + ' Power Out (dBm): 21.01', + ' Delta_P (dB): 0.90', + ' target pch (dBm): None', + ' actual pch out (dBm): -1.77', + ' output VOA (dB): 3.00']) + assert actual_c_amp == expected_c_amp + actual_l_amp = amp.amplifiers["LBAND"].__str__() + expected_l_amp = '\n'.join([ + 'Edfa Edfa1', + ' type_variety: std_medium_gain_L', + ' effective gain(dB): 21.00', + ' (before att_in and before output VOA)', + ' noise figure (dB): 6.36', + ' (including att_in)', + ' pad att_in (dB): 0.00', + ' Power In (dBm): -1.61', + ' Power Out (dBm): 19.40', + ' Delta_P (dB): 3.00', + ' target pch (dBm): None', + ' actual pch out (dBm): -1.99', + ' output VOA (dB): 3.00']) + assert actual_l_amp == expected_l_amp + + # check that f_min, f_max of si are within amp band + assert amp.amplifiers["LBAND"].params.f_min == 186.55e12 + assert si.frequency[0] >= amp.amplifiers["LBAND"].params.f_min + assert amp.amplifiers["CBAND"].params.f_max == 196.15e12 + assert si.frequency[-1] <= amp.amplifiers["CBAND"].params.f_max + for freq in si.frequency: + if freq > 190.05e12: + assert freq >= 191.25e12 + if freq < 191.25e12: + assert freq <= 190.25e12 diff --git a/tests/test_invocation.py b/tests/test_invocation.py index 9f18a1af5..8ac6cd557 100644 --- a/tests/test_invocation.py +++ b/tests/test_invocation.py @@ -36,7 +36,7 @@ ('transmission_long_psd', None, transmission_main_example, ['-e', 'tests/data/eqpt_config_psd.json', 'tests/data/test_long_network.json', '--spectrum', 'gnpy/example-data/initial_spectrum2.json', ]), ('transmission_long_psw', None, transmission_main_example, - ['-e', 'tests/data/eqpt_config_psw.json', 'tests/data/test_long_network.json', '--spectrum', 'gnpy/example-data/initial_spectrum2.json', ]), + ['-e', 'tests/data/eqpt_config_psw.json', 'tests/data/test_long_network.json', '--spectrum', 'gnpy/example-data/initial_spectrum2.json', ]) )) def test_example_invocation(capfd, caplog, output, log, handler, args): """Make sure that our examples produce useful output"""