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 <esther.lerouzic@orange.com>
Change-Id: I392f06c792af9f32d4a14324c683bd3fae655de8
This commit is contained in:
EstherLerouzic
2025-02-17 18:01:24 +01:00
parent 4df6cc6b23
commit f447c908bc
5 changed files with 208 additions and 68 deletions

View File

@@ -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():

View File

@@ -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'])

View File

@@ -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": {

View File

@@ -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)

View File

@@ -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,