From f447c908bc0ba2bb6d07007dd2b5fd9b6961688f Mon Sep 17 00:00:00 2001 From: EstherLerouzic Date: Mon, 17 Feb 2025 18:01:24 +0100 Subject: [PATCH] Feat: Add spacing info in the design_band info This will be used to compute the max total power for design per OMS. Signed-off-by: EstherLerouzic Change-Id: I392f06c792af9f32d4a14324c683bd3fae655de8 --- gnpy/core/network.py | 55 ++++-- gnpy/core/utils.py | 172 ++++++++++++++---- .../multiband_example_network.json | 8 +- gnpy/topology/request.py | 3 +- tests/test_network_functions.py | 38 ++-- 5 files changed, 208 insertions(+), 68 deletions(-) diff --git a/gnpy/core/network.py b/gnpy/core/network.py index d7c1cbaf..579b9a6b 100644 --- a/gnpy/core/network.py +++ b/gnpy/core/network.py @@ -26,7 +26,7 @@ from gnpy.core import elements from gnpy.core.equipment import find_type_variety, find_type_varieties from gnpy.core.exceptions import ConfigurationError, NetworkTopologyError from gnpy.core.utils import round2float, convert_length, psd2powerdbm, lin2db, watt2dbm, dbm2watt, automatic_nch, \ - find_common_range + find_common_range, get_spacing_from_band, reorder_per_degree_design_bands from gnpy.core.info import ReferenceCarrier, create_input_spectral_information from gnpy.core.parameters import SimParams, EdfaParams, find_band_name, FrequencyBand, MultiBandParams from gnpy.core.science_utils import RamanSolver @@ -1330,8 +1330,11 @@ def set_per_degree_design_band(node: Union[elements.Roadm, elements.Transceiver] """ next_oms = (n for n in network.successors(node)) if len(node.design_bands) == 0: - node.design_bands = [{'f_min': si.f_min, 'f_max': si.f_max} for si in equipment['SI'].values()] - + node.design_bands = [{'f_min': si.f_min, 'f_max': si.f_max, 'spacing': si.spacing} + for si in equipment['SI'].values()] + # complete node.per_degree_design_bands with node.design_bands spacing when it is missing. + # Use the spacing from SI if nothing is specified. + update_design_bands_spacing(node, equipment['SI']['default'].spacing) default_is_single_band = len(node.design_bands) == 1 for next_node in next_oms: # get all the elements from the OMS and retrieve their amps types and bands @@ -1347,14 +1350,14 @@ def set_per_degree_design_band(node: Union[elements.Roadm, elements.Transceiver] if oms_is_single_band == default_is_single_band: amp_bands.append(node.design_bands) - common_range = find_common_range(amp_bands, None, None) + common_range = find_common_range(amp_bands, None, None, equipment['SI']['default'].spacing, node.design_bands) # node.per_degree_design_bands has already been populated with node.params.per_degree_design_bands loaded # from the json. # let's complete the dict with the design band of degrees for which there was no definition if next_node.uid not in node.per_degree_design_bands: if common_range: # if degree design band was not defined, then use the common_range computed with the oms amplifiers - # already defined + # already defined. node.per_degree_design_bands[next_node.uid] = common_range elif oms_is_single_band is None or (oms_is_single_band == default_is_single_band): # else if no amps are defined (no bands) then use default ROADM bands @@ -1364,14 +1367,42 @@ def set_per_degree_design_band(node: Union[elements.Roadm, elements.Transceiver] # unsupported case: single band OMS with default multiband design band raise NetworkTopologyError(f"in {node.uid} degree {next_node.uid}: inconsistent design multiband/" + " single band definition on a single band/ multiband OMS") - if next_node.uid in node.params.per_degree_design_bands: - # order bands per min frequency in params.per_degree_design_bands for those degree that are defined there - node.params.per_degree_design_bands[next_node.uid] = \ - sorted(node.params.per_degree_design_bands[next_node.uid], key=lambda x: x['f_min']) - # order the bands per min frequency in .per_degree_design_bands (all degrees must exist there) - node.per_degree_design_bands[next_node.uid] = \ - sorted(node.per_degree_design_bands[next_node.uid], key=lambda x: x['f_min']) + # reorder per_degree_design_bands. + reorder_per_degree_design_bands(node.per_degree_design_bands) + reorder_per_degree_design_bands(node.params.per_degree_design_bands) # check node.params.per_degree_design_bands keys + check_per_degree_design_bands_keys(node, network) + + +def update_design_bands_spacing(node: Union[elements.Roadm, elements.Transceiver], + default_spacing: float): + """ + Update the spacing of design bands for a given node. + + This function iterates through the design bands associated with the node and updates + their spacing based on the frequency range defined by 'f_min' and 'f_max'. If a specific + spacing cannot be determined from the design bands, the default spacing is used. + + :param node: The node object which can be either a Roadm or Transceiver instance. + :type node: Union[elements.Roadm, elements.Transceiver] + :param default_spacing: The default spacing to use if no specific spacing can be determined. + :type default_spacing: float + """ + for design_bands in node.per_degree_design_bands.values(): + for design_band in design_bands: + temp = get_spacing_from_band(node.design_bands, design_band['f_min'], design_band['f_max']) + default_spacing = temp if temp is not None else default_spacing + design_band['spacing'] = design_band.get('spacing', default_spacing) + + +def check_per_degree_design_bands_keys(node: Union[elements.Roadm, elements.Transceiver], network: DiGraph): + """Checks that per_degree_design_bands keys are existing uid of elements in the network + + :param node: a ROADM or a Transceiver element + :type node: Union[elements.Roadm, elements.Transceiver] + :param network: the network containing the node and its connections + :type network: DiGraph + """ if node.params.per_degree_design_bands: next_oms_uid = [n.uid for n in network.successors(node)] for degree in node.params.per_degree_design_bands.keys(): diff --git a/gnpy/core/utils.py b/gnpy/core/utils.py index 57fcfeb3..9c19df47 100644 --- a/gnpy/core/utils.py +++ b/gnpy/core/utils.py @@ -17,7 +17,7 @@ from csv import writer from numpy import pi, cos, sqrt, log10, linspace, zeros, shape, where, logical_and, mean, array from scipy import constants from copy import deepcopy -from typing import List, Union +from typing import List, Union, Dict from gnpy.core.exceptions import ConfigurationError @@ -628,49 +628,124 @@ def nice_column_str(data: List[List[str]], max_length: int = 30, padding: int = 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 - If there are no amplifiers in the path, then use default band - - >>> amp_bands = [[{'f_min': 191e12, 'f_max' : 195e12}, {'f_min': 186e12, 'f_max' : 190e12} ], \ - [{'f_min': 185e12, 'f_max' : 189e12}, {'f_min': 192e12, 'f_max' : 196e12}], \ - [{'f_min': 186e12, 'f_max': 193e12}]] - >>> find_common_range(amp_bands, 190e12, 195e12) - [{'f_min': 186000000000000.0, 'f_max': 189000000000000.0}, {'f_min': 192000000000000.0, 'f_max': 193000000000000.0}] - >>> amp_bands = [[{'f_min': 191e12, 'f_max' : 195e12}, {'f_min': 186e12, 'f_max' : 190e12} ], \ - [{'f_min': 185e12, 'f_max' : 189e12}, {'f_min': 192e12, 'f_max' : 196e12}], \ - [{'f_min': 186e12, 'f_max': 192e12}]] - >>> find_common_range(amp_bands, 190e12, 195e12) - [{'f_min': 186000000000000.0, 'f_max': 189000000000000.0}] +def filter_valid_amp_bands(amp_bands: List[List[dict]]) -> List[List[dict]]: + """Filter out invalid amplifier bands that lack f_min or f_max. + :param amp_bands: A list of lists containing amplifier band dictionaries. + :type amp_bands: List[List[dict]] + :return: A filtered list of amplifier bands that contain valid f_min and f_max. + :rtype: List[List[dict]] """ - _amp_bands = [sorted(amp, key=lambda x: x['f_min']) for amp in amp_bands] - _temp = [] - # remove None bands - for amp in _amp_bands: - is_band = True - for band in amp: - if not (is_band and band['f_min'] and band['f_max']): - is_band = False - if is_band: - _temp.append(amp) + return [amp for amp in amp_bands if all(band.get('f_min') is not None and band.get('f_max') is not None + for band in amp)] - # remove duplicate + +def remove_duplicates(amp_bands: List[List[dict]]) -> List[List[dict]]: + """Remove duplicate amplifier bands. + + :param amp_bands: A list of lists containing amplifier band dictionaries. + :type amp_bands: List[List[dict]] + :return: A list of unique amplifier bands. + :rtype: List[List[dict]] + """ unique_amp_bands = [] - for amp in _temp: + for amp in amp_bands: if amp not in unique_amp_bands: unique_amp_bands.append(amp) + return unique_amp_bands + + +def calculate_spacing(first: dict, second: dict, default_spacing: float, default_design_bands: Union[List[Dict], None], + f_min: float, f_max: float) -> float: + """Calculate the spacing for the given frequency range. + + :param first: The first amplifier band dictionary. + :type first: dict + :param second: The second amplifier band dictionary. + :type second: dict + :param default_spacing: The default spacing to use if no specific spacing can be determined. + :type default_spacing: float + :param default_design_bands: Optional list of design bands to determine spacing from. + :type default_design_bands: Union[List[Dict], None] + :param f_min: The minimum frequency of the range. + :type f_min: float + :param f_max: The maximum frequency of the range. + :type f_max: float + :return: The calculated spacing for the given frequency range. + :rtype: float + """ + if first.get('spacing') is not None and second.get('spacing') is not None: + return max(first['spacing'], second['spacing']) + elif first.get('spacing') is not None: + return first['spacing'] + elif second.get('spacing') is not None: + return second['spacing'] + elif default_design_bands: + temp = get_spacing_from_band(default_design_bands, f_min, f_max) + return temp if temp is not None else default_spacing + return default_spacing + + +def find_common_range(amp_bands: List[List[dict]], default_band_f_min: Union[float, None], + default_band_f_max: Union[float, None], default_spacing: float, + default_design_bands: Union[List[Dict], None] = None) -> List[dict]: + """ + Find the common frequency range of amplifier bands. + + If there are no amplifiers in the path, then use the default band parameters. + + :param amp_bands: A list of lists containing amplifier band dictionaries, each with 'f_min', 'f_max', + and optionally 'spacing'. + :type amp_bands: List[List[dict]] + :param default_band_f_min: The minimum frequency of the default band. + :type default_band_f_min: Union[float, None] + :param default_band_f_max: The maximum frequency of the default band. + :type default_band_f_max: Union[float, None] + :param default_spacing: The default spacing to use if no specific spacing can be determined. + :type default_spacing: float + :param default_design_bands: Optional list of design bands to determine spacing from. + :type default_design_bands: Union[List[Dict], None] + :return: A list of dictionaries representing the common frequency ranges with their respective spacings. + :rtype: List[dict] + + >>> amp_bands = [[{'f_min': 191e12, 'f_max' : 195e12, 'spacing': 70e9}, {'f_min': 186e12, 'f_max' : 190e12}], \ + [{'f_min': 185e12, 'f_max' : 189e12}, {'f_min': 192e12, 'f_max' : 196e12}], \ + [{'f_min': 186e12, 'f_max': 193e12}]] + >>> find_common_range(amp_bands, 190e12, 195e12, 50e9) + [{'f_min': 186000000000000.0, 'f_max': 189000000000000.0, 'spacing': 50000000000.0}, \ +{'f_min': 192000000000000.0, 'f_max': 193000000000000.0, 'spacing': 70000000000.0}] + + >>> amp_bands = [[{'f_min': 191e12, 'f_max' : 195e12}, {'f_min': 186e12, 'f_max' : 190e12}], \ + [{'f_min': 185e12, 'f_max' : 189e12}, {'f_min': 192e12, 'f_max' : 196e12}], \ + [{'f_min': 186e12, 'f_max': 192e12}]] + >>> find_common_range(amp_bands, 190e12, 195e12, 50e9) + [{'f_min': 186000000000000.0, 'f_max': 189000000000000.0, 'spacing': 50000000000.0}] + """ + # Step 1: Filter and sort amplifier bands + _amp_bands = [sorted(amp, key=lambda x: x['f_min']) for amp in filter_valid_amp_bands(amp_bands)] + unique_amp_bands = remove_duplicates(_amp_bands) + + # Step 2: Handle cases with no valid bands if unique_amp_bands: common_range = unique_amp_bands[0] else: if default_band_f_min is None or default_band_f_max is None: return [] - common_range = [{'f_min': default_band_f_min, 'f_max': default_band_f_max}] + return [{'f_min': default_band_f_min, 'f_max': default_band_f_max, 'spacing': None}] + + # Step 3: Calculate common frequency range for bands in unique_amp_bands: - common_range = [{'f_min': max(first['f_min'], second['f_min']), 'f_max': min(first['f_max'], second['f_max'])} - for first in common_range for second in bands - if max(first['f_min'], second['f_min']) < min(first['f_max'], second['f_max'])] + new_common_range = [] + for first in common_range: + for second in bands: + f_min = max(first['f_min'], second['f_min']) + f_max = min(first['f_max'], second['f_max']) + if f_min < f_max: + spacing = calculate_spacing(first, second, default_spacing, default_design_bands, f_min, f_max) + new_common_range.append({'f_min': f_min, 'f_max': f_max, 'spacing': spacing}) + + common_range = new_common_range + return sorted(common_range, key=lambda x: x['f_min']) @@ -715,3 +790,36 @@ def convert_pmd_lineic(pmd: Union[float, None], length: float, length_unit: str) if pmd: return pmd * 1e-12 / sqrt(convert_length(length, length_unit)) return None +def get_spacing_from_band(design_bands: List[Dict], f_min, f_max): + """Retrieve the spacing for a frequency range based on design bands. + + This function checks if the midpoint of the provided frequency range (f_min, f_max) + falls within any of the design bands. If it does, the corresponding spacing is returned. + + :param design_bands: A list of design band dictionaries, each containing 'f_min', 'f_max', and 'spacing'. + :type design_bands: List[Dict] + :param f_min: The minimum frequency of the range. + :type f_min: float + :param f_max: The maximum frequency of the range. + :type f_max: float + :return: The spacing corresponding to the design band that contains the midpoint of the range, + or None if no such band exists. + :rtype: Union[float, None] + """ + midpoint = (f_min + f_max) / 2 + for band in design_bands: + if midpoint >= band['f_min'] and midpoint <= band['f_max']: + return band['spacing'] + return None + + +def reorder_per_degree_design_bands(per_degree_design_bands: dict): + """Sort the design bands for each degree by their minimum frequency (f_min). + + This function modifies the input dictionary in place, sorting the design bands for each unique identifier. + + :param per_degree_design_bands: A dictionary where keys are unique identifiers and values are lists of design band dictionaries. + :type per_degree_design_bands: Dict[str, List[Dict]] + """ + for uid, design_bands in per_degree_design_bands.items(): + per_degree_design_bands[uid] = sorted(design_bands, key=lambda x: x['f_min']) diff --git a/gnpy/example-data/multiband_example_network.json b/gnpy/example-data/multiband_example_network.json index 5ac804c6..f1e181d7 100644 --- a/gnpy/example-data/multiband_example_network.json +++ b/gnpy/example-data/multiband_example_network.json @@ -52,7 +52,7 @@ "preamp_variety_list": [], "booster_variety_list": [] }, - "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12}] + "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12, "spacing": 50e9}] }, "metadata": { "location": { @@ -71,7 +71,7 @@ "preamp_variety_list": [], "booster_variety_list": [] }, - "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12}] + "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12, "spacing": 50e9}] }, "metadata": { "location": { @@ -90,7 +90,7 @@ "preamp_variety_list": [], "booster_variety_list": [] }, - "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12}] + "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12, "spacing": 50e9}] }, "metadata": { "location": { @@ -109,7 +109,7 @@ "preamp_variety_list": [], "booster_variety_list": [] }, - "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12}] + "design_bands": [{"f_min": 191.3e12, "f_max": 195.1e12, "spacing": 50e9}] }, "metadata": { "location": { diff --git a/gnpy/topology/request.py b/gnpy/topology/request.py index 117c0a79..f173a130 100644 --- a/gnpy/topology/request.py +++ b/gnpy/topology/request.py @@ -1305,4 +1305,5 @@ def find_elements_common_range(el_list: list, equipment: dict) -> List[dict]: 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, Multiband_amplifier))] - return find_common_range(amp_bands, equipment['SI']['default'].f_min, equipment['SI']['default'].f_max) + return find_common_range(amp_bands, equipment['SI']['default'].f_min, equipment['SI']['default'].f_max, + equipment['SI']['default'].spacing) diff --git a/tests/test_network_functions.py b/tests/test_network_functions.py index 30f39377..331d10bd 100644 --- a/tests/test_network_functions.py +++ b/tests/test_network_functions.py @@ -619,14 +619,14 @@ def network_base(case, site_type, length=50.0, amplifier_type='Multiband_amplifi elif case == 'monoband_roadm': roadm1['params'] = { 'design_bands': [ - {'f_min': 192.3e12, 'f_max': 196.0e12} + {'f_min': 192.3e12, 'f_max': 196.0e12, 'spacing': 50e9} ] } elif case == 'monoband_per_degree': roadm1['params'] = { 'per_degree_design_bands': { 'east edfa in SITE1 to ILA1': [ - {'f_min': 191.5e12, 'f_max': 195.0e12} + {'f_min': 191.5e12, 'f_max': 195.0e12, 'spacing': 50e9} ] } } @@ -670,19 +670,19 @@ def network_base(case, site_type, length=50.0, amplifier_type='Multiband_amplifi @pytest.mark.parametrize('case, site_type, amplifier_type, expected_design_bands, expected_per_degree_design_bands', [ ('monoband_no_design_band', 'Edfa', 'Edfa', - [{'f_min': 191.3e12, 'f_max': 196.1e12}], [{'f_min': 191.3e12, 'f_max': 196.1e12}]), + [{'f_min': 191.3e12, 'f_max': 196.1e12, 'spacing': 50e9}], [{'f_min': 191.3e12, 'f_max': 196.1e12, 'spacing': 50e9}]), ('monoband_roadm', 'Edfa', 'Edfa', - [{'f_min': 192.3e12, 'f_max': 196.0e12}], [{'f_min': 192.3e12, 'f_max': 196.0e12}]), + [{'f_min': 192.3e12, 'f_max': 196.0e12, 'spacing': 50e9}], [{'f_min': 192.3e12, 'f_max': 196.0e12, 'spacing': 50e9}]), ('monoband_per_degree', 'Edfa', 'Edfa', - [{'f_min': 191.3e12, 'f_max': 196.1e12}], [{'f_min': 191.5e12, 'f_max': 195.0e12}]), + [{'f_min': 191.3e12, 'f_max': 196.1e12, 'spacing': 50e9}], [{'f_min': 191.5e12, 'f_max': 195.0e12, 'spacing': 50e9}]), ('monoband_design', 'Edfa', 'Edfa', - [{'f_min': 191.3e12, 'f_max': 196.1e12}], [{'f_min': 191.3e12, 'f_max': 196.1e12}]), + [{'f_min': 191.3e12, 'f_max': 196.1e12, 'spacing': 50e9}], [{'f_min': 191.3e12, 'f_max': 196.1e12, 'spacing': 50e9}]), ('design', 'Fused', 'Multiband_amplifier', - [{'f_min': 191.3e12, 'f_max': 196.1e12}], - [{'f_min': 186.55e12, 'f_max': 190.05e12}, {'f_min': 191.25e12, 'f_max': 196.15e12}]), + [{'f_min': 191.3e12, 'f_max': 196.1e12, 'spacing': 50e9}], + [{'f_min': 186.55e12, 'f_max': 190.05e12, 'spacing': 50e9}, {'f_min': 191.25e12, 'f_max': 196.15e12, 'spacing': 50e9}]), ('no_design', 'Fused', 'Multiband_amplifier', - [{'f_min': 191.3e12, 'f_max': 196.1e12}], - [{'f_min': 187.0e12, 'f_max': 190.0e12}, {'f_min': 191.3e12, 'f_max': 196.0e12}])]) + [{'f_min': 191.3e12, 'f_max': 196.1e12, 'spacing': 50e9}], + [{'f_min': 187.0e12, 'f_max': 190.0e12, 'spacing': 50e9}, {'f_min': 191.3e12, 'f_max': 196.0e12, 'spacing': 50e9}])]) def test_design_band(case, site_type, amplifier_type, expected_design_bands, expected_per_degree_design_bands): """Check design_band is the one defined: - in SI if nothing is defined, @@ -964,20 +964,20 @@ def network_wo_booster(site_type, bands): @pytest.mark.parametrize('site_type, expected_type, bands, expected_bands', [ ('Multiband_amplifier', Multiband_amplifier, - [{'f_min': 187.0e12, 'f_max': 190.0e12}, {'f_min': 191.3e12, 'f_max': 196.0e12}], - [{'f_min': 187.0e12, 'f_max': 190.0e12}, {'f_min': 191.3e12, 'f_max': 196.0e12}]), + [{'f_min': 187.0e12, 'f_max': 190.0e12, "spacing": 50e9}, {'f_min': 191.3e12, 'f_max': 196.0e12, "spacing": 50e9}], + [{'f_min': 187.0e12, 'f_max': 190.0e12, "spacing": 50e9}, {'f_min': 191.3e12, 'f_max': 196.0e12, "spacing": 50e9}]), ('Edfa', Edfa, - [{'f_min': 191.4e12, 'f_max': 196.1e12}], - [{'f_min': 191.4e12, 'f_max': 196.1e12}]), + [{'f_min': 191.4e12, 'f_max': 196.1e12, "spacing": 50e9}], + [{'f_min': 191.4e12, 'f_max': 196.1e12, "spacing": 50e9}]), ('Edfa', Edfa, - [{'f_min': 191.2e12, 'f_max': 196.0e12}], + [{'f_min': 191.2e12, 'f_max': 196.0e12, "spacing": 50e9}], []), ('Fused', Multiband_amplifier, - [{'f_min': 187.0e12, 'f_max': 190.0e12}, {'f_min': 191.3e12, 'f_max': 196.0e12}], - [{'f_min': 187.0e12, 'f_max': 190.0e12}, {'f_min': 191.3e12, 'f_max': 196.0e12}]), + [{'f_min': 187.0e12, 'f_max': 190.0e12, "spacing": 50e9}, {'f_min': 191.3e12, 'f_max': 196.0e12, "spacing": 50e9}], + [{'f_min': 187.0e12, 'f_max': 190.0e12, "spacing": 50e9}, {'f_min': 191.3e12, 'f_max': 196.0e12, "spacing": 50e9}]), ('Fused', Edfa, - [{'f_min': 191.3e12, 'f_max': 196.0e12}], - [{'f_min': 191.3e12, 'f_max': 196.0e12}])]) + [{'f_min': 191.3e12, 'f_max': 196.0e12, "spacing": 50e9}], + [{'f_min': 191.3e12, 'f_max': 196.0e12, "spacing": 50e9}])]) def test_insert_amp(site_type, expected_type, bands, expected_bands): """Check: - if amplifiers are defined in multiband they are used for design,