mirror of
				https://github.com/Telecominfraproject/oopt-gnpy.git
				synced 2025-10-31 18:18:00 +00:00 
			
		
		
		
	 f2039fbe1c
			
		
	
	f2039fbe1c
	
	
	
		
			
			In order to be used by API. Co-authored-by: Renato Ambrosone <renato.ambrosone@polito.it> Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com> Change-Id: I12111427c8a90b85b3158cdd95f4ee771cb39316
		
			
				
	
	
		
			1058 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1058 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python3
 | |
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| # SPDX-License-Identifier: BSD-3-Clause
 | |
| # gnpy.tools.json_io: Loading and saving data from JSON files in GNPy's internal data format
 | |
| # Copyright (C) 2025 Telecom Infra Project and GNPy contributors
 | |
| # see AUTHORS.rst for a list of contributors
 | |
| 
 | |
| """
 | |
| gnpy.tools.json_io
 | |
| ==================
 | |
| 
 | |
| Loading and saving data from JSON files in GNPy's internal data format
 | |
| """
 | |
| 
 | |
| from logging import getLogger
 | |
| from pathlib import Path
 | |
| import json
 | |
| from collections import namedtuple
 | |
| from copy import deepcopy
 | |
| from typing import Union, Dict, List, Tuple, Optional
 | |
| from networkx import DiGraph
 | |
| from numpy import arange
 | |
| 
 | |
| 
 | |
| from gnpy.core import elements
 | |
| 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, use_pmd_coef
 | |
| from gnpy.core.parameters import DEFAULT_RAMAN_COEFFICIENT, EdfaParams, MultiBandParams, DEFAULT_EDFA_CONFIG
 | |
| from gnpy.topology.request import PathRequest, Disjunction, compute_spectrum_slot_vs_bandwidth, ResultElement
 | |
| from gnpy.topology.spectrum_assignment import mvalue_to_slots
 | |
| from gnpy.tools.convert import xls_to_json_data
 | |
| from gnpy.tools.service_sheet import read_service_sheet
 | |
| from gnpy.tools.convert_legacy_yang import yang_to_legacy, legacy_to_yang
 | |
| from gnpy.tools.default_edfa_config import DEFAULT_EXTRA_CONFIG, DEFAULT_EQPT_CONFIG
 | |
| 
 | |
| 
 | |
| _logger = getLogger(__name__)
 | |
| 
 | |
| 
 | |
| Model_vg = namedtuple('Model_vg', 'nf1 nf2 delta_p orig_nf_min orig_nf_max')
 | |
| Model_fg = namedtuple('Model_fg', 'nf0')
 | |
| Model_openroadm_ila = namedtuple('Model_openroadm_ila', 'nf_coef')
 | |
| Model_hybrid = namedtuple('Model_hybrid', 'nf_ram gain_ram edfa_variety')
 | |
| Model_dual_stage = namedtuple('Model_dual_stage', 'preamp_variety booster_variety')
 | |
| 
 | |
| 
 | |
| class Model_openroadm_preamp:
 | |
|     """class to hold nf model specific to OpenROADM preamp
 | |
|     """
 | |
| 
 | |
| 
 | |
| class Model_openroadm_booster:
 | |
|     """class to hold nf model specific to OpenROADM booster
 | |
|     """
 | |
| 
 | |
| 
 | |
| class _JsonThing:
 | |
|     """Base class for json equipment
 | |
|     """
 | |
|     def update_attr(self, default_values, kwargs, name):
 | |
|         """Build the attributes based on kwargs dict
 | |
|         """
 | |
|         clean_kwargs = {k: v for k, v in kwargs.items() if v != ''}
 | |
|         for k, v in default_values.items():
 | |
|             setattr(self, k, clean_kwargs.get(k, v))
 | |
|             disable_warning_keys = ['use_si_channel_count_for_design', 'voa_step', 'voa_margin', 'span_loss_ref',
 | |
|                                     'power_slope']
 | |
|             if k not in clean_kwargs and name != 'Amp' and v is not None and v != [] and k not in disable_warning_keys:
 | |
|                 # do not show this warning if the default value is None
 | |
|                 msg = f'\n\tWARNING missing {k} attribute in eqpt_config.json[{name}]' \
 | |
|                     + f'\n\tdefault value is {k} = {v}\n'
 | |
|                 _logger.warning(msg)
 | |
| 
 | |
| 
 | |
| class SI(_JsonThing):
 | |
|     """Spectrum Information
 | |
|     """
 | |
|     default_values = {
 | |
|         "f_min": 191.35e12,
 | |
|         "f_max": 196.1e12,
 | |
|         "baud_rate": 32e9,
 | |
|         "spacing": 50e9,
 | |
|         "power_dbm": 0,
 | |
|         "power_range_db": [0, 0, 0.5],
 | |
|         "roll_off": 0.15,
 | |
|         "tx_osnr": 45,
 | |
|         "sys_margins": 0,
 | |
|         "tx_power_dbm": None,  # optional value in SI
 | |
|         "use_si_channel_count_for_design": False  # optional value in SI
 | |
|     }
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         self.update_attr(self.default_values, kwargs, 'SI')
 | |
| 
 | |
| 
 | |
| class Span(_JsonThing):
 | |
|     """Span simulations definition
 | |
|     """
 | |
|     default_values = {
 | |
|         'power_mode': True,
 | |
|         'delta_power_range_db': None,
 | |
|         'max_fiber_lineic_loss_for_raman': 0.25,
 | |
|         'target_extended_gain': 2.5,
 | |
|         'max_length': 150,
 | |
|         'length_units': 'km',
 | |
|         'max_loss': None,
 | |
|         'padding': 10,
 | |
|         'EOL': 0,
 | |
|         'con_in': 0,
 | |
|         'con_out': 0,
 | |
|         "span_loss_ref": 20.0,
 | |
|         "power_slope": 0.3,
 | |
|         "voa_margin": 1,
 | |
|         "voa_step": 0.5
 | |
|     }
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         self.update_attr(self.default_values, kwargs, 'Span')
 | |
| 
 | |
| 
 | |
| class Roadm(_JsonThing):
 | |
|     """List of ROADM and their specs
 | |
|     """
 | |
|     default_values = {
 | |
|         'type_variety': 'default',
 | |
|         'add_drop_osnr': 100,
 | |
|         'pmd': 0,
 | |
|         'pdl': 0,
 | |
|         'restrictions': {
 | |
|             'preamp_variety_list': [],
 | |
|             'booster_variety_list': []
 | |
|         },
 | |
|         'roadm-path-impairments': []
 | |
|     }
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         # If equalization is not defined in equipment, then raise an error.
 | |
|         # Only one type of equalization must be defined.
 | |
|         allowed_equalisations = ['target_pch_out_db', 'target_psd_out_mWperGHz', 'target_out_mWperSlotWidth']
 | |
|         requested_eq_mask = [eq in kwargs for eq in allowed_equalisations]
 | |
|         if sum(requested_eq_mask) > 1:
 | |
|             msg = 'Only one equalization type should be set in ROADM, found: ' \
 | |
|                   + ', '.join(eq for eq in allowed_equalisations if eq in kwargs)
 | |
|             raise EquipmentConfigError(msg)
 | |
|         if not any(requested_eq_mask):
 | |
|             msg = 'No equalization type set in ROADM'
 | |
|             raise EquipmentConfigError(msg)
 | |
|         for key in allowed_equalisations:
 | |
|             if key in kwargs:
 | |
|                 setattr(self, key, kwargs[key])
 | |
|                 break
 | |
|         self.update_attr(self.default_values, kwargs, 'Roadm')
 | |
| 
 | |
| 
 | |
| class Transceiver(_JsonThing):
 | |
|     """List of transceivers and their modes
 | |
|     """
 | |
|     default_values = {
 | |
|         'type_variety': None,
 | |
|         'frequency': None,
 | |
|         'mode': {}
 | |
|     }
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         self.update_attr(self.default_values, kwargs, 'Transceiver')
 | |
|         for mode_params in self.mode:
 | |
|             penalties = mode_params.get('penalties')
 | |
|             mode_params['penalties'] = {}
 | |
|             mode_params['equalization_offset_db'] = mode_params.get('equalization_offset_db', 0)
 | |
|             if not penalties:
 | |
|                 continue
 | |
|             for impairment in ('chromatic_dispersion', 'pmd', 'pdl'):
 | |
|                 imp_penalties = [p for p in penalties if impairment in p]
 | |
|                 if not imp_penalties:
 | |
|                     continue
 | |
|                 if all(p[impairment] > 0 for p in imp_penalties):
 | |
|                     # make sure the list of penalty values include a proper lower boundary
 | |
|                     # (we assume 0 penalty for 0 impairment)
 | |
|                     imp_penalties.insert(0, {impairment: 0, 'penalty_value': 0})
 | |
|                 # make sure the list of penalty values are sorted by impairment value
 | |
|                 imp_penalties.sort(key=lambda i: i[impairment])
 | |
|                 # rearrange as dict of lists instead of list of dicts
 | |
|                 mode_params['penalties'][impairment] = {
 | |
|                     'up_to_boundary': [p[impairment] for p in imp_penalties],
 | |
|                     'penalty_value': [p['penalty_value'] for p in imp_penalties]
 | |
|                 }
 | |
| 
 | |
| 
 | |
| class Fiber(_JsonThing):
 | |
|     """Fiber default settings
 | |
|     """
 | |
|     default_values = {
 | |
|         'type_variety': '',
 | |
|         'dispersion': None,
 | |
|         'effective_area': None,
 | |
|         'pmd_coef': 0
 | |
|     }
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         self.update_attr(self.default_values, kwargs, self.__class__.__name__)
 | |
|         if 'gamma' in kwargs:
 | |
|             setattr(self, 'gamma', kwargs['gamma'])
 | |
|         if 'raman_efficiency' in kwargs:
 | |
|             raman_coefficient = kwargs['raman_efficiency']
 | |
|             cr = raman_coefficient.pop('cr')
 | |
|             raman_coefficient['g0'] = cr
 | |
|             raman_coefficient['reference_frequency'] = DEFAULT_RAMAN_COEFFICIENT['reference_frequency']
 | |
|             setattr(self, 'raman_coefficient', raman_coefficient)
 | |
| 
 | |
| 
 | |
| class RamanFiber(Fiber):
 | |
|     """Raman Fiber default settings
 | |
|     """
 | |
| 
 | |
| 
 | |
| class Amp(_JsonThing):
 | |
|     """List of amplifiers with their specs
 | |
|     """
 | |
|     default_values = EdfaParams.default_values
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         self.update_attr(self.default_values, kwargs, 'Amp')
 | |
| 
 | |
|     @classmethod
 | |
|     def from_json(cls, extra_configs, **kwargs):
 | |
|         """
 | |
|         """
 | |
|         # default EDFA DGT and ripples are defined in parameters DEFAULT_EDFA_CONFIG. copy these values when
 | |
|         # creating a new amplifier
 | |
|         config = {k: v for k, v in DEFAULT_EDFA_CONFIG.items()}
 | |
|         config_filename = 'default'    # default value to display in case of error
 | |
|         type_variety = kwargs['type_variety']
 | |
|         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':
 | |
|             if 'default_config_from_json' in kwargs:
 | |
|                 # use user defined default instead of DEFAULT_EDFA_CONFIG
 | |
|                 config_filename = kwargs.pop('default_config_from_json')
 | |
|                 config = deepcopy(extra_configs[config_filename])
 | |
|             try:
 | |
|                 nf0 = kwargs.pop('nf0')
 | |
|             except KeyError as exc:  # nf0 is expected for a fixed gain amp
 | |
|                 msg = f'missing nf0 value input for amplifier: {type_variety} in equipment config'
 | |
|                 raise EquipmentConfigError(msg) from exc
 | |
|             for k in ('nf_min', 'nf_max'):
 | |
|                 try:
 | |
|                     del kwargs[k]
 | |
|                 except KeyError:
 | |
|                     pass
 | |
|             nf_def = Model_fg(nf0)
 | |
|         elif type_def == 'advanced_model':
 | |
|             # use the user file name define in library instead of default config
 | |
|             config_filename = kwargs.pop('advanced_config_from_json')
 | |
|             config = deepcopy(extra_configs[config_filename])
 | |
|         elif type_def == 'variable_gain':
 | |
|             if 'default_config_from_json' in kwargs:
 | |
|                 # use user defined default instead of DEFAULT_EDFA_CONFIG
 | |
|                 config_filename = kwargs.pop('default_config_from_json')
 | |
|                 config = deepcopy(extra_configs[config_filename])
 | |
|             gain_min, gain_max = kwargs['gain_min'], kwargs['gain_flatmax']
 | |
|             try:  # nf_min and nf_max are expected for a variable gain amp
 | |
|                 nf_min = kwargs.pop('nf_min')
 | |
|                 nf_max = kwargs.pop('nf_max')
 | |
|             except KeyError as exc:
 | |
|                 msg = f'missing nf_min or nf_max value input for amplifier: {type_variety} in equipment config'
 | |
|                 raise EquipmentConfigError(msg) from exc
 | |
|             try:  # remove all remaining nf inputs
 | |
|                 del kwargs['nf0']
 | |
|             except KeyError:
 | |
|                 pass  # nf0 is not needed for variable gain amp
 | |
|             nf1, nf2, delta_p = estimate_nf_model(type_variety, gain_min, gain_max, nf_min, nf_max)
 | |
|             nf_def = Model_vg(nf1, nf2, delta_p, nf_min, nf_max)
 | |
|         elif type_def == 'openroadm':
 | |
|             try:
 | |
|                 nf_coef = kwargs.pop('nf_coef')
 | |
|             except KeyError as exc:  # nf_coef is expected for openroadm amp
 | |
|                 raise EquipmentConfigError(f'missing nf_coef input for amplifier: {type_variety} in equipment config') from exc
 | |
|             nf_def = Model_openroadm_ila(nf_coef)
 | |
|         elif type_def == 'openroadm_preamp':
 | |
|             nf_def = Model_openroadm_preamp()
 | |
|         elif type_def == 'openroadm_booster':
 | |
|             nf_def = Model_openroadm_booster()
 | |
|         elif type_def == 'dual_stage':
 | |
|             try:  # nf_ram and gain_ram are expected for a hybrid amp
 | |
|                 preamp_variety = kwargs.pop('preamp_variety')
 | |
|                 booster_variety = kwargs.pop('booster_variety')
 | |
|             except KeyError as exc:
 | |
|                 raise EquipmentConfigError(f'missing preamp/booster variety input for amplifier: {type_variety}'
 | |
|                                            + ' in equipment config') from exc
 | |
|             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')
 | |
| 
 | |
|         # raise an error if config does not contain f_min, f_max
 | |
|         if 'f_min' not in config or 'f_max' not in config:
 | |
|             raise EquipmentConfigError(f'Config file {config_filename} does not contain f_min and f_max values.'
 | |
|                                        + ' Please correct file.')
 | |
|         # use f_min, f_max from kwargs
 | |
|         if 'f_min' in kwargs:
 | |
|             config.pop('f_min', None)
 | |
|             config.pop('f_max', None)
 | |
|         return cls(**{**kwargs, **config,
 | |
|                       'nf_model': nf_def, 'dual_stage_model': dual_stage_def, 'multi_band': amplifiers})
 | |
| 
 | |
| 
 | |
| def _spectrum_from_json(json_data: dict):
 | |
|     """JSON_data is a list of spectrum partitions each with
 | |
|     {f_min, f_max, baud_rate, roll_off, delta_pdb, slot_width, tx_osnr, label}
 | |
|     Creates the per freq Carrier's dict.
 | |
|     f_min, f_max, baud_rate, slot_width and roll_off are mandatory
 | |
|     label, tx_osnr and delta_pdb are created if not present
 | |
|     label should be different for each partition
 | |
|     >>> json_data = {'spectrum': \
 | |
|         [{'f_min': 193.2e12, 'f_max': 193.4e12, 'slot_width': 50e9, 'baud_rate': 32e9, 'roll_off': 0.15, \
 | |
|             'delta_pdb': 1, 'tx_osnr': 45, 'tx_power_dbm': -7},\
 | |
|         {'f_min': 193.4625e12, 'f_max': 193.9875e12, 'slot_width': 75e9, 'baud_rate': 64e9, 'roll_off': 0.15},\
 | |
|         {'f_min': 194.075e12, 'f_max': 194.075e12, 'slot_width': 100e9, 'baud_rate': 90e9, 'roll_off': 0.15},\
 | |
|         {'f_min': 194.2e12, 'f_max': 194.35e12, 'slot_width': 50e9, 'baud_rate': 32e9, 'roll_off': 0.15}]}
 | |
|     >>> spectrum = _spectrum_from_json(json_data['spectrum'])
 | |
|     >>> for k, v in spectrum.items():
 | |
|     ...     print(f'{k}: {v}')
 | |
|     ...
 | |
|     193200000000000.0: Carrier(delta_pdb=1, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=45, tx_power=0.00019952623149688798, label='0-32.00G')
 | |
|     193250000000000.0: Carrier(delta_pdb=1, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=45, tx_power=0.00019952623149688798, label='0-32.00G')
 | |
|     193300000000000.0: Carrier(delta_pdb=1, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=45, tx_power=0.00019952623149688798, label='0-32.00G')
 | |
|     193350000000000.0: Carrier(delta_pdb=1, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=45, tx_power=0.00019952623149688798, label='0-32.00G')
 | |
|     193400000000000.0: Carrier(delta_pdb=1, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=45, tx_power=0.00019952623149688798, label='0-32.00G')
 | |
|     193462500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     193537500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     193612500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     193687500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     193762500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     193837500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     193912500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     193987500000000.0: Carrier(delta_pdb=0, baud_rate=64000000000.0, slot_width=75000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='1-64.00G')
 | |
|     194075000000000.0: Carrier(delta_pdb=0, baud_rate=90000000000.0, slot_width=100000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='2-90.00G')
 | |
|     194200000000000.0: Carrier(delta_pdb=0, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='3-32.00G')
 | |
|     194250000000000.0: Carrier(delta_pdb=0, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='3-32.00G')
 | |
|     194300000000000.0: Carrier(delta_pdb=0, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='3-32.00G')
 | |
|     194350000000000.0: Carrier(delta_pdb=0, baud_rate=32000000000.0, slot_width=50000000000.0, roll_off=0.15, tx_osnr=40, tx_power=0.001, label='3-32.00G')
 | |
|     """
 | |
|     spectrum = {}
 | |
|     json_data = sorted(json_data, key=lambda x: x['f_min'])
 | |
|     # min freq of occupation is f_min - slot_width/2 (numbering starts at 0)
 | |
|     previous_part_max_freq = 0.0
 | |
|     for index, part in enumerate(json_data):
 | |
|         # default delta_pdb is 0 dB
 | |
|         part.setdefault('delta_pdb', 0)
 | |
|         # add a label to the partition for the printings
 | |
|         part.setdefault('label', f'{index}-{part["baud_rate"] * 1e-9:.2f}G')
 | |
|         # default tx_osnr is set to 40 dB
 | |
|         part.setdefault('tx_osnr', 40)
 | |
|         # default tx_power_dbm is set to 0 dBn
 | |
|         part.setdefault('tx_power_dbm', 0)
 | |
|         # starting freq is exactly f_min to be consistent with utils.automatic_nch
 | |
|         # first partition min occupation is f_min - slot_width / 2 (central_frequency is f_min)
 | |
|         # supposes that carriers are centered on frequency
 | |
|         if previous_part_max_freq > (part['f_min'] - part['slot_width'] / 2):
 | |
|             # check that previous part last channel does not overlap on next part first channel
 | |
|             # max center of the part should be below part['f_max'] and aligned on the slot_width
 | |
|             msg = 'Not a valid initial spectrum definition:\nprevious spectrum last carrier max occupation ' +\
 | |
|                 f'{previous_part_max_freq * 1e-12:.5f}GHz ' +\
 | |
|                 'overlaps on next spectrum first carrier occupation ' +\
 | |
|                 f'{(part["f_min"] - part["slot_width"] / 2) * 1e-12:.5f}GHz'
 | |
|             raise ValueError(msg)
 | |
| 
 | |
|         max_range = ((part['f_max'] - part['f_min']) // part['slot_width'] + 1) * part['slot_width']
 | |
|         previous_part_max_freq = None
 | |
|         for current_freq in arange(part['f_min'],
 | |
|                                    part['f_min'] + max_range,
 | |
|                                    part['slot_width']):
 | |
|             spectrum[current_freq] = Carrier(delta_pdb=part['delta_pdb'], baud_rate=part['baud_rate'],
 | |
|                                              slot_width=part['slot_width'], roll_off=part['roll_off'],
 | |
|                                              tx_osnr=part['tx_osnr'], tx_power=dbm2watt(part['tx_power_dbm']),
 | |
|                                              label=part['label'])
 | |
|         previous_part_max_freq = current_freq + part['slot_width'] / 2
 | |
|     return spectrum
 | |
| 
 | |
| 
 | |
| def merge_equipment(equipment: Dict, extra_equipments: Dict[str, Dict], extra_configs: Dict[str, Dict]):
 | |
|     """Merge additional equipment libraries into the base equipment dictionary.
 | |
|     Typical case is the use of third party transceivers which are not part of a the supplier library.
 | |
| 
 | |
|     raise warnings if the same reference is used on two different libraries
 | |
|     """
 | |
|     for filename, json_data in extra_equipments.items():
 | |
|         extra_eqpt = _equipment_from_json(json_data, extra_configs)
 | |
|         # populate with default eqpt to streamline loading
 | |
|         for eqpt_type, extra_items in extra_eqpt.items():
 | |
|             for type_variety, item in extra_items.items():
 | |
|                 if type_variety not in equipment[eqpt_type]:
 | |
|                     equipment[eqpt_type][type_variety] = item
 | |
|                 else:
 | |
|                     msg = f'\n\tEquipment file {filename}: duplicate equipment entry found: {eqpt_type}-{type_variety}\n'
 | |
|                     _logger.warning(msg)
 | |
| 
 | |
| 
 | |
| def load_equipments_and_configs(equipment_filename: Path,
 | |
|                                 extra_equipment_filenames: List[Path],
 | |
|                                 extra_config_filenames: List[Path]) -> Dict:
 | |
|     """Loads equipment configurations and merge with additional equipment and configuration files.
 | |
| 
 | |
|     :param equipment_filename: The path to the primary equipment configuration file.
 | |
|     :type equipment_filename: Path
 | |
|     :param extra_equipment_filenames: A list of paths to additional equipment configuration files to merge.
 | |
|     :type extra_equipment_filenames: List[Path]
 | |
|     :param extra_config_filenames: A list of paths to additional configuration files to include.
 | |
|     :type extra_config_filenames: List[Path]
 | |
|     :return: A dictionary containing the loaded equipment configurations.
 | |
|     :rtype: Dict
 | |
| 
 | |
|     Notes:
 | |
|         If no equipment filename is provided, a default equipment configuration will be used.
 | |
|         Additional configurations from `extra_config_filenames` will override the default configurations.
 | |
|         If `extra_equipment_filenames` are provided, their contents will be merged into the loaded equipment.
 | |
|     """
 | |
|     extra_configs = DEFAULT_EXTRA_CONFIG
 | |
|     if not equipment_filename:
 | |
|         equipment_filename = DEFAULT_EQPT_CONFIG
 | |
|     if extra_config_filenames:
 | |
|         # All files must have different filenames (as filename is used as the key in the library)
 | |
|         filename_list = [f.name for f in extra_config_filenames]
 | |
|         if len(set(filename_list)) != len(extra_config_filenames):
 | |
|             msg = f'Identical filenames for extra-config {filename_list}'
 | |
|             _logger.error(msg)
 | |
|             raise ConfigurationError(msg)
 | |
|         extra_configs = {f.name: load_json(f) for f in extra_config_filenames}
 | |
|         for k, v in DEFAULT_EXTRA_CONFIG.items():
 | |
|             extra_configs[k] = v
 | |
|     equipment = load_equipment(equipment_filename, extra_configs)
 | |
|     if extra_equipment_filenames:
 | |
|         # use the string representation of the path to support identical filenames but placed in different folders.
 | |
|         extra_equipments = {f.as_posix(): load_json(f) for f in extra_equipment_filenames}
 | |
|         merge_equipment(equipment, extra_equipments, extra_configs)
 | |
|     return equipment
 | |
| 
 | |
| 
 | |
| def load_equipment(filename: Path, extra_configs: Dict[str, Dict] = DEFAULT_EXTRA_CONFIG) -> Dict:
 | |
|     """Load equipment, returns equipment dict
 | |
|     """
 | |
|     json_data = load_gnpy_json(filename)
 | |
|     return _equipment_from_json(json_data, extra_configs)
 | |
| 
 | |
| 
 | |
| def load_initial_spectrum(filename: Path) -> dict:
 | |
|     """Load spectrum to propagate, returns spectrum dict
 | |
|     """
 | |
|     json_data = load_gnpy_json(filename)
 | |
|     return _spectrum_from_json(json_data['spectrum'])
 | |
| 
 | |
| 
 | |
| def _update_dual_stage(equipment: dict):
 | |
|     """Update attributes of all dual stage amps with the preamp and booster attributes
 | |
|     (defined in the equipment dictionary)
 | |
| 
 | |
|     Returns the updated equiment dictionary
 | |
|     """
 | |
|     if 'Edfa' not in equipment:
 | |
|         return
 | |
|     edfa_dict = equipment['Edfa']
 | |
|     for edfa in edfa_dict.values():
 | |
|         if edfa.type_def == 'dual_stage':
 | |
|             edfa_preamp = edfa_dict[edfa.dual_stage_model.preamp_variety]
 | |
|             edfa_booster = edfa_dict[edfa.dual_stage_model.booster_variety]
 | |
|             for key, value in edfa_preamp.__dict__.items():
 | |
|                 attr_k = 'preamp_' + key
 | |
|                 setattr(edfa, attr_k, value)
 | |
|             for key, value in edfa_booster.__dict__.items():
 | |
|                 attr_k = 'booster_' + key
 | |
|                 setattr(edfa, attr_k, value)
 | |
|             edfa.p_max = edfa_booster.p_max
 | |
|             edfa.gain_flatmax = edfa_booster.gain_flatmax + edfa_preamp.gain_flatmax
 | |
|             if edfa.gain_min < edfa_preamp.gain_min:
 | |
|                 raise EquipmentConfigError(f'Dual stage {edfa.type_variety} minimal gain is lower than its preamp minimal gain')
 | |
|     return equipment
 | |
| 
 | |
| 
 | |
| def _update_band(equipment: dict):
 | |
|     """Creates a list of bands for this amplifier, and remove other parameters which are not applicable
 | |
|     """
 | |
|     if 'Edfa' not in equipment:
 | |
|         return
 | |
|     amp_dict = equipment['Edfa']
 | |
|     for amplifier in amp_dict.values():
 | |
|         if amplifier.type_def != 'multi_band':
 | |
|             amplifier.bands = [{'f_min': amplifier.f_min,
 | |
|                                 'f_max': amplifier.f_max}]
 | |
|             # updates band parameter
 | |
|         else:
 | |
|             _bands = [{'f_min': amp_dict[a].f_min,
 | |
|                        'f_max': amp_dict[a].f_max} for a in amp_dict[amplifier.type_variety].multi_band]
 | |
|             # remove duplicates
 | |
|             amplifier.bands = []
 | |
|             for b in _bands:
 | |
|                 if b not in amplifier.bands:
 | |
|                     amplifier.bands.append(b)
 | |
|             # remove non applicable parameters
 | |
|             for key in ['f_min', 'f_max', 'gain_flatmax', 'gain_min', 'p_max', 'nf_model', 'dual_stage_model',
 | |
|                         'nf_fit_coeff', 'nf_ripple', 'dgt', 'gain_ripple']:
 | |
|                 delattr(amplifier, key)
 | |
| 
 | |
| 
 | |
| def _roadm_restrictions_sanity_check(equipment: dict):
 | |
|     """verifies that booster and preamp restrictions specified in roadm equipment are listed in the edfa."""
 | |
|     if 'Roadm' not in equipment:
 | |
|         return equipment
 | |
|     for roadm_type, roadm_eqpt in equipment['Roadm'].items():
 | |
|         restrictions = roadm_eqpt.restrictions['booster_variety_list'] + \
 | |
|             roadm_eqpt.restrictions['preamp_variety_list']
 | |
|         for amp_name in restrictions:
 | |
|             if amp_name not in equipment['Edfa']:
 | |
|                 raise EquipmentConfigError(f'ROADM {roadm_type} restriction {amp_name} does not refer to a '
 | |
|                                            + 'defined EDFA name')
 | |
| 
 | |
| 
 | |
| def _check_fiber_vs_raman_fiber(equipment: dict):
 | |
|     """Ensure that Fiber and RamanFiber with the same name define common properties equally"""
 | |
|     if 'RamanFiber' not in equipment:
 | |
|         return
 | |
|     for fiber_type in set(equipment['Fiber'].keys()) & set(equipment['RamanFiber'].keys()):
 | |
|         for attr in ('dispersion', 'dispersion-slope', 'effective_area', 'gamma', 'pmd-coefficient'):
 | |
|             fiber = equipment['Fiber'][fiber_type]
 | |
|             raman = equipment['RamanFiber'][fiber_type]
 | |
|             a = getattr(fiber, attr, None)
 | |
|             b = getattr(raman, attr, None)
 | |
|             if a != b:
 | |
|                 raise EquipmentConfigError(f'WARNING: Fiber and RamanFiber definition of "{fiber_type}" '
 | |
|                                            f'disagrees for "{attr}": {a} != {b}')
 | |
| 
 | |
| 
 | |
| def _si_sanity_check(equipment):
 | |
|     """Check that 'default' key correctly exists in SI list. (There must be at list one element and it must be default)
 | |
|     If not create one entry in the list with this key.
 | |
|     """
 | |
|     if 'SI' not in equipment:
 | |
|         return
 | |
|     possible_SI = list(equipment['SI'].keys())
 | |
|     if 'default' not in possible_SI:
 | |
|         # Use "default" key in the equipment, using the first listed keys
 | |
|         equipment['SI']['default'] = equipment['SI'][possible_SI[0]]
 | |
|         del equipment['SI'][possible_SI[0]]
 | |
| 
 | |
| 
 | |
| def _equipment_from_json(json_data: dict, extra_configs: Dict[str, Dict]) -> Dict:
 | |
|     """build global dictionnary eqpt_library that stores all eqpt characteristics:
 | |
|     edfa type type_variety, fiber type_variety
 | |
|     from the eqpt_config.json (filename parameter)
 | |
|     also read advanced_config_from_json file parameters for edfa if they are available:
 | |
|     typically nf_ripple, dfg gain ripple, dgt and nf polynomial nf_fit_coeff
 | |
|     if advanced_config_from_json file parameter is not present: use nf_model:
 | |
|     requires nf_min and nf_max values boundaries of the edfa gain range
 | |
|     """
 | |
|     equipment = {}
 | |
|     for key, entries in json_data.items():
 | |
|         equipment[key] = {}
 | |
|         for entry in entries:
 | |
|             subkey = entry.get('type_variety', 'default')
 | |
|             if key == 'Edfa':
 | |
|                 equipment[key][subkey] = Amp.from_json(extra_configs, **entry)
 | |
|             elif key == 'Fiber':
 | |
|                 equipment[key][subkey] = Fiber(**entry)
 | |
|             elif key == 'Span':
 | |
|                 equipment[key][subkey] = Span(**entry)
 | |
|             elif key == 'Roadm':
 | |
|                 equipment[key][subkey] = Roadm(**entry)
 | |
|             elif key == 'SI':
 | |
|                 # use power_dbm value for tx_power_dbm if the key is not in 'SI'
 | |
|                 # if 'tx_power_dbm' not in entry.keys():
 | |
|                 #     entry['tx_power_dbm'] = entry['power_dbm']
 | |
|                 equipment[key][subkey] = SI(**entry)
 | |
|             elif key == 'Transceiver':
 | |
|                 equipment[key][subkey] = Transceiver(**entry)
 | |
|             elif key == 'RamanFiber':
 | |
|                 equipment[key][subkey] = RamanFiber(**entry)
 | |
|             else:
 | |
|                 raise EquipmentConfigError(f'Unrecognized network element type "{key}"')
 | |
|     _check_fiber_vs_raman_fiber(equipment)
 | |
|     _update_dual_stage(equipment)
 | |
|     _update_band(equipment)
 | |
|     _roadm_restrictions_sanity_check(equipment)
 | |
|     _si_sanity_check(equipment)
 | |
|     return equipment
 | |
| 
 | |
| 
 | |
| def load_network(filename: Path, equipment: dict) -> DiGraph:
 | |
|     """load network json or excel
 | |
| 
 | |
|     :param filename: input file to read from
 | |
|     :param equipment: equipment library
 | |
|     """
 | |
|     if filename.suffix.lower() in ('.xls', '.xlsx'):
 | |
|         json_data = xls_to_json_data(filename)
 | |
|     elif filename.suffix.lower() == '.json':
 | |
|         json_data = load_gnpy_json(filename)
 | |
|     else:
 | |
|         raise ValueError(f'unsupported topology filename extension {filename.suffix.lower()}')
 | |
|     return network_from_json(json_data, equipment)
 | |
| 
 | |
| 
 | |
| def save_network(network: DiGraph, filename: str):
 | |
|     """Dump the network into a JSON file
 | |
| 
 | |
|     :param network: network to work on
 | |
|     :param filename: file to write to
 | |
|     """
 | |
|     save_json(network_to_json(network), filename)
 | |
| 
 | |
| 
 | |
| def _cls_for(equipment_type):
 | |
|     if equipment_type == 'Edfa':
 | |
|         return elements.Edfa
 | |
|     if equipment_type == 'Fused':
 | |
|         return elements.Fused
 | |
|     if equipment_type == 'Roadm':
 | |
|         return elements.Roadm
 | |
|     if equipment_type == 'Transceiver':
 | |
|         return elements.Transceiver
 | |
|     if equipment_type == 'Fiber':
 | |
|         return elements.Fiber
 | |
|     if equipment_type == 'RamanFiber':
 | |
|         return elements.RamanFiber
 | |
|     if equipment_type == 'Multiband_amplifier':
 | |
|         return elements.Multiband_amplifier
 | |
|     raise ConfigurationError(f'Unknown network equipment "{equipment_type}"')
 | |
| 
 | |
| 
 | |
| def network_from_json(json_data: dict, equipment: dict) -> DiGraph:
 | |
|     """create digraph based on json input dict and using equipment library to fill in the gaps
 | |
|     """
 | |
|     # NOTE|dutc: we could use the following, but it would tie our data format
 | |
|     #            too closely to the graph library
 | |
|     # from networkx import node_link_graph
 | |
|     g = DiGraph()
 | |
|     g.graph['network_name'] = json_data.get('network_name', None)
 | |
|     for el_config in json_data['elements']:
 | |
|         typ = el_config.pop('type')
 | |
|         variety = el_config.pop('type_variety', 'default')
 | |
|         cls = _cls_for(typ)
 | |
|         if typ == 'Transceiver':
 | |
|             temp = el_config.setdefault('params', {})
 | |
|         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 not in 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]:
 | |
|             extra_params = equipment[typ][variety].__dict__
 | |
|             temp = el_config.setdefault('params', {})
 | |
|             if typ == 'Roadm':
 | |
|                 # if equalization is defined, remove default equalization from the extra_params
 | |
|                 # If equalisation is not defined in the element config, then use the default one from equipment
 | |
|                 # if more than one equalization was defined in element config, then raise an error
 | |
|                 extra_params = merge_equalization(temp, extra_params)
 | |
|                 if not extra_params:
 | |
|                     msg = f'ROADM {el_config["uid"]}: invalid equalization settings'
 | |
|                     raise ConfigurationError(msg)
 | |
|             # use temp pmd_coef if it exists else use the default one from library and keep this knowledge in
 | |
|             # pmd_coef_defined
 | |
|             use_pmd_coef(temp, extra_params)
 | |
|             temp = merge_amplifier_restrictions(temp, extra_params)
 | |
|             el_config['params'] = temp
 | |
|             el_config['type_variety'] = variety
 | |
|         elif (typ in ['Fiber', 'RamanFiber', 'Roadm']):
 | |
|             raise ConfigurationError(f'The {typ} of variety type {variety} was not recognized:'
 | |
|                                      '\nplease check it is properly defined in the eqpt_config json file')
 | |
|         elif typ == 'Edfa':
 | |
|             if variety in ['default', '']:
 | |
|                 el_config['params'] = Amp.default_values
 | |
|             else:
 | |
|                 raise ConfigurationError(f'The Edfa of variety type {variety} was not recognized:'
 | |
|                                          '\nplease check it is properly defined in the eqpt_config json file')
 | |
|         el = cls(**el_config)
 | |
|         g.add_node(el)
 | |
| 
 | |
|     nodes = {k.uid: k for k in g.nodes()}
 | |
| 
 | |
|     for cx in json_data['connections']:
 | |
|         from_node, to_node = cx['from_node'], cx['to_node']
 | |
|         try:
 | |
|             if isinstance(nodes[from_node], elements.Fiber):
 | |
|                 edge_length = nodes[from_node].params.length
 | |
|             else:
 | |
|                 edge_length = 0.01
 | |
|             g.add_edge(nodes[from_node], nodes[to_node], weight=edge_length)
 | |
|         except KeyError as exc:
 | |
|             msg = f'can not find {from_node} or {to_node} defined in {cx}'
 | |
|             raise NetworkTopologyError(msg) from exc
 | |
| 
 | |
|     return g
 | |
| 
 | |
| 
 | |
| def network_to_json(network: DiGraph) -> dict:
 | |
|     """Export network graph as a json dict
 | |
|     """
 | |
|     data = {
 | |
|         'elements': [n.to_json for n in network]
 | |
|     }
 | |
|     connections = {
 | |
|         'connections': [{"from_node": n.uid,
 | |
|                          "to_node": next_n.uid}
 | |
|                         for n in network
 | |
|                         for next_n in network.successors(n) if next_n is not None]
 | |
|     }
 | |
|     data.update(connections)
 | |
|     if network.graph['network_name']:
 | |
|         data['network_name'] = network.graph['network_name']
 | |
|     return data
 | |
| 
 | |
| 
 | |
| def load_json(filename: Path) -> dict:
 | |
|     """load json data
 | |
| 
 | |
|     :param filename: Path to the file to convert
 | |
|     :type filemname: Path
 | |
|     :return: json data in a dictionnary
 | |
|     :rtype: Dict
 | |
|     """
 | |
|     with open(filename, 'r', encoding='utf-8') as f:
 | |
|         data = json.load(f)
 | |
|     return data
 | |
| 
 | |
| 
 | |
| def load_gnpy_json(filename: Path) -> dict:
 | |
|     """load json data. It supports both legacy ang yang formatted inputs based on yang models.
 | |
| 
 | |
|     :param filename: Path to the file to convert
 | |
|     :type filemname: Path
 | |
|     :return: json data in a dictionnary
 | |
|     :rtype: Dict
 | |
|     """
 | |
|     return yang_to_legacy(load_json(filename))
 | |
| 
 | |
| 
 | |
| def save_json(obj: dict, filename: Path):
 | |
|     """Save in json format. Export yang formatted data (RFC7951)
 | |
|     """
 | |
|     data = legacy_to_yang(obj)
 | |
|     with open(filename, 'w', encoding='utf-8') as f:
 | |
|         json.dump(obj, f, indent=2, ensure_ascii=False)
 | |
| 
 | |
| 
 | |
| def load_requests(filename: Path, eqpt: dict, bidir: bool, network: DiGraph, network_filename: str) -> dict:
 | |
|     """loads the requests from a json or an excel file into a data string"""
 | |
|     if filename.suffix.lower() in ('.xls', '.xlsx'):
 | |
|         _logger.info('Automatically converting requests from XLS to JSON')
 | |
|         try:
 | |
|             return convert_service_sheet(filename, eqpt, network, network_filename=network_filename, bidir=bidir)
 | |
|         except ServiceError as this_e:
 | |
|             raise ServiceError(f'Service error: {this_e}') from this_e
 | |
|     else:
 | |
|         return load_json(filename)
 | |
| 
 | |
| 
 | |
| def requests_from_json(json_data: dict, equipment: dict) -> List[PathRequest]:
 | |
|     """Extract list of requests from data parsed from JSON"""
 | |
|     requests_list = []
 | |
| 
 | |
|     for req in json_data['path-request']:
 | |
|         # init all params from request
 | |
|         trx_type = req['path-constraints']['te-bandwidth']['trx_type']
 | |
|         trx_mode = req['path-constraints']['te-bandwidth'].get('trx_mode', None)
 | |
|         if trx_type is None:
 | |
|             msg = f'Request {req["request-id"]} has no transceiver type defined.'
 | |
|             raise ServiceError(msg)
 | |
|         try:
 | |
|             nd_list = sorted(req['explicit-route-objects']['route-object-include-exclude'], key=lambda x: x['index'])
 | |
|         except KeyError:
 | |
|             nd_list = []
 | |
|         params = {
 | |
|             'request_id': f'{req["request-id"]}',
 | |
|             'source': req['source'],
 | |
|             'destination': req['destination'],
 | |
|             'bidir': req['bidirectional'],
 | |
|             'trx_type': trx_type,
 | |
|             'trx_mode': trx_mode,
 | |
|             'format': trx_mode,
 | |
|             'spacing': req['path-constraints']['te-bandwidth']['spacing'],
 | |
|             'nodes_list': [n['num-unnum-hop']['node-id'] for n in nd_list],
 | |
|             'loose_list': [n['num-unnum-hop']['hop-type'] for n in nd_list]
 | |
|         }
 | |
|         # recover trx physical param (baudrate, ...) from type and mode
 | |
|         # nb_channel is computed based on min max frequency and spacing
 | |
|         try:
 | |
|             trx_params = trx_mode_params(equipment, params['trx_type'], params['trx_mode'], True)
 | |
|         except EquipmentConfigError as e:
 | |
|             msg = f'Equipment Config error in {req["request-id"]}: {e}'
 | |
|             raise EquipmentConfigError(msg) from e
 | |
|         params.update(trx_params)
 | |
|         params['power'] = req['path-constraints']['te-bandwidth'].get('output-power')
 | |
|         # params must not be None, but user can set to None: catch this case
 | |
|         if params['power'] is None:
 | |
|             params['power'] = dbm2watt(equipment['SI']['default'].power_dbm)
 | |
| 
 | |
|         # same process for nb-channel
 | |
|         f_min = params['f_min']
 | |
|         f_max_from_si = params['f_max']
 | |
|         try:
 | |
|             if req['path-constraints']['te-bandwidth']['max-nb-of-channel'] is not None:
 | |
|                 nch = req['path-constraints']['te-bandwidth']['max-nb-of-channel']
 | |
|                 params['nb_channel'] = nch
 | |
|                 spacing = params['spacing']
 | |
|                 params['f_max'] = automatic_fmax(f_min, spacing, nch)
 | |
|             else:
 | |
|                 params['nb_channel'] = automatic_nch(f_min, f_max_from_si, params['spacing'])
 | |
|         except KeyError:
 | |
|             params['nb_channel'] = automatic_nch(f_min, f_max_from_si, params['spacing'])
 | |
|         params['effective_freq_slot'] = \
 | |
|             req['path-constraints']['te-bandwidth'].get('effective-freq-slot', [{'N': None, 'M': None}])
 | |
|         try:
 | |
|             params['path_bandwidth'] = req['path-constraints']['te-bandwidth']['path_bandwidth']
 | |
|         except KeyError:
 | |
|             pass
 | |
|         params['tx_power'] = req['path-constraints']['te-bandwidth'].get('tx_power')
 | |
|         default_tx_power_dbm = equipment['SI']['default'].tx_power_dbm
 | |
|         if params['tx_power'] is None:
 | |
|             # use request's input power in span instead
 | |
|             params['tx_power'] = params['power']
 | |
|             if default_tx_power_dbm is not None:
 | |
|                 # use default tx power
 | |
|                 params['tx_power'] = dbm2watt(default_tx_power_dbm)
 | |
|         _check_one_request(params, f_max_from_si)
 | |
|         requests_list.append(PathRequest(**params))
 | |
|     return requests_list
 | |
| 
 | |
| 
 | |
| def _check_one_request(params: dict, f_max_from_si: float):
 | |
|     """Checks that the requested parameters are consistant (spacing vs nb channel vs transponder mode...)"""
 | |
|     f_min = params['f_min']
 | |
|     f_max = params['f_max']
 | |
|     max_recommanded_nb_channels = automatic_nch(f_min, f_max_from_si, params['spacing'])
 | |
|     if params['baud_rate'] is not None:
 | |
|         # implicitly means that a mode is defined with min_spacing
 | |
|         if params['min_spacing'] > params['spacing']:
 | |
|             msg = f'Request {params["request_id"]} has spacing below transponder ' +\
 | |
|                   f'{params["trx_type"]} {params["trx_mode"]} min spacing value ' +\
 | |
|                   f'{params["min_spacing"] * 1e-9}GHz.\nComputation stopped'
 | |
|             raise ServiceError(msg)
 | |
|         if f_max > f_max_from_si:
 | |
|             msg = f'Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"] * 1e-9} GHz' \
 | |
|                   + f' and requested spacing {params["spacing"] * 1e-9}GHz is not consistent with frequency range' \
 | |
|                   + f' {f_min * 1e-12} THz, {f_max_from_si * 1e-12} THz.' \
 | |
|                   + f' Max recommanded nb of channels is {max_recommanded_nb_channels}.'
 | |
|             raise ServiceError(msg)
 | |
|     # Transponder mode already selected; will it fit to the requested bandwidth?
 | |
|     if params['trx_mode'] is not None and params['effective_freq_slot'] is not None:
 | |
|         required_nb_of_channels, _ = compute_spectrum_slot_vs_bandwidth(params['path_bandwidth'],
 | |
|                                                                         params['spacing'],
 | |
|                                                                         params['bit_rate'])
 | |
|         _, per_channel_m = compute_spectrum_slot_vs_bandwidth(params['bit_rate'],
 | |
|                                                               params['spacing'],
 | |
|                                                               params['bit_rate'])
 | |
|         # each M should fit one or more channels if it is not None
 | |
|         # spectrum slots should not overlap
 | |
|         # resulting nb of channels should be bigger than the nb computed with path_bandwidth
 | |
|         # without being splitted
 | |
|         # TODO: elaborate a more accurate estimate with nb_wl * tx_osnr + possibly guardbands in case of
 | |
|         # superchannel closed packing.
 | |
|         nb_of_channels = 0
 | |
|         # order slots
 | |
|         slots = sorted(params['effective_freq_slot'], key=lambda x: float('inf') if x['N'] is None else x['N'])
 | |
|         for slot in slots:
 | |
|             nb_of_channels = nb_of_channels + slot['M'] // per_channel_m if slot['M'] is not None \
 | |
|                 and nb_of_channels is not None else None
 | |
|             if slot['M'] is not None and slot['M'] < per_channel_m:
 | |
|                 msg = f'Requested M {slot} number of slots for request' +\
 | |
|                       f' {params["request_id"]} should be greater than {per_channel_m} to support request' +\
 | |
|                       f'with {params["trx_type"]} {params["trx_mode"]}'
 | |
|                 _logger.critical(msg)
 | |
|         if nb_of_channels is not None and nb_of_channels < required_nb_of_channels:
 | |
|             msg = f'Requested M {slots} number of slots for request {params["request_id"]} support {nb_of_channels}' +\
 | |
|                   f' nb of channels while {required_nb_of_channels} are required to support request' +\
 | |
|                   f' {params["path_bandwidth"] * 1e-9} Gbit/s with {params["trx_type"]} {params["trx_mode"]}'
 | |
|             raise ServiceError(msg)
 | |
|         if nb_of_channels is not None:
 | |
|             _, stop0n = mvalue_to_slots(slots[0]['N'], slots[0]['M'])
 | |
|             i = 1
 | |
|             while i < len(slots):
 | |
|                 slot = slots[i]
 | |
|                 startn, stopn = mvalue_to_slots(slot['N'], slot['M'])
 | |
|                 if startn <= stop0n:
 | |
|                     msg = f'Requested M {slots} for request {params["request_id"]} overlap'
 | |
|                     raise ServiceError(msg)
 | |
|                 _, stop0n = startn, stopn
 | |
|                 i += 1
 | |
| 
 | |
| 
 | |
| def disjunctions_from_json(json_data: dict) -> List[Disjunction]:
 | |
|     """reads the disjunction requests from the json dict and create the list
 | |
|     of requested disjunctions for this set of requests
 | |
|     """
 | |
|     disjunctions_list = []
 | |
|     if 'synchronization' in json_data:
 | |
|         for snc in json_data['synchronization']:
 | |
|             params = {}
 | |
|             params['disjunction_id'] = snc['synchronization-id']
 | |
|             params['relaxable'] = snc['svec']['relaxable']
 | |
|             params['link_diverse'] = 'link' in snc['svec']['disjointness']
 | |
|             params['node_diverse'] = 'node' in snc['svec']['disjointness']
 | |
|             params['disjunctions_req'] = snc['svec']['request-id-number']
 | |
|             disjunctions_list.append(Disjunction(**params))
 | |
| 
 | |
|     return disjunctions_list
 | |
| 
 | |
| 
 | |
| def convert_service_sheet(
 | |
|         input_filename: Path,
 | |
|         eqpt: dict,
 | |
|         network: DiGraph,
 | |
|         network_filename: Union[Path, None] = None,
 | |
|         output_filename: str = '',
 | |
|         bidir: bool = False):
 | |
|     """Converts xls into json format services
 | |
| 
 | |
|     :param input_filename: xls(x) file containing the service sheet
 | |
|     :param eqpt: equipment library
 | |
|     :param network: network for which these services apply (required for xls inputs to correct names)
 | |
|     :param network_filename: optional network file name that was used for network creation
 | |
|                              (required for xls inputs to correct names)
 | |
|     :param output_filename: name of the file where converted data are savec
 | |
|     :param bidir: set all services bidir attribute with this bool
 | |
|     """
 | |
|     if output_filename == '':
 | |
|         output_filename = f'{str(input_filename)[0:len(str(input_filename)) - len(str(input_filename.suffixes[0]))]}_services.json'
 | |
|     data = read_service_sheet(input_filename, eqpt, network, network_filename, bidir)
 | |
|     save_json(data, output_filename)
 | |
|     return data
 | |
| 
 | |
| 
 | |
| def find_equalisation(params: Dict, equalization_types: List[str]):
 | |
|     """Find the equalization(s) defined in params. params can be a dict or a Roadm object.
 | |
| 
 | |
|     >>> roadm = {'add_drop_osnr': 100, 'pmd': 1, 'pdl': 0.5,
 | |
|     ...     'restrictions': {'preamp_variety_list': ['a'], 'booster_variety_list': ['b']},
 | |
|     ...     'target_psd_out_mWperGHz': 4e-4}
 | |
|     >>> equalization_types = ['target_pch_out_db', 'target_psd_out_mWperGHz']
 | |
|     >>> find_equalisation(roadm, equalization_types)
 | |
|     {'target_pch_out_db': False, 'target_psd_out_mWperGHz': True}
 | |
|     """
 | |
|     equalization = {e: False for e in equalization_types}
 | |
|     for equ in equalization_types:
 | |
|         if equ in params:
 | |
|             equalization[equ] = True
 | |
|     return equalization
 | |
| 
 | |
| 
 | |
| def merge_equalization(params: dict, extra_params: dict) -> Union[dict, None]:
 | |
|     """params contains ROADM element config and extra_params default values from equipment library.
 | |
|     If equalization is not defined in ROADM element use the one defined in equipment library.
 | |
|     Only one type of equalization must be defined: power (target_pch_out_db) or PSD (target_psd_out_mWperGHz)
 | |
|     or PSW (target_out_mWperSlotWidth)
 | |
|     params and extra_params are dict
 | |
|     """
 | |
|     equalization_types = ['target_pch_out_db', 'target_psd_out_mWperGHz', 'target_out_mWperSlotWidth']
 | |
|     roadm_equalizations = find_equalisation(params, equalization_types)
 | |
|     if sum(roadm_equalizations.values()) > 1:
 | |
|         # if ROADM config contains more than one equalization type then this is an error
 | |
|         return None
 | |
|     if sum(roadm_equalizations.values()) == 1:
 | |
|         # if ROADM config contains one equalization
 | |
|         # don't use the default equalization
 | |
|         return {k: v for k, v in extra_params.items() if k not in equalization_types}
 | |
|     if sum(roadm_equalizations.values()) == 0:
 | |
|         # If ROADM config doesn't contain any equalization type, keep the default one
 | |
|         return extra_params
 | |
|     return None
 | |
| 
 | |
| 
 | |
| def results_to_json(pathresults: List[ResultElement]):
 | |
|     """Converts a list of `ResultElement` objects into a JSON-compatible dictionary.
 | |
| 
 | |
|     :param pathresults: List of `ResultElement` objects to be converted.
 | |
|     :return: A dictionary with a single key `"response"`, containing a list of
 | |
|              the `json` attributes of the provided `ResultElement` objects.
 | |
|     """
 | |
|     return {'response': [n.json for n in pathresults]}
 | |
| 
 | |
| 
 | |
| def load_eqpt_topo_from_json(eqpt: dict, topology: dict, extra_equipments: Optional[Dict[str, Dict]] = None,
 | |
|                              extra_configs: Dict[str, Dict] = DEFAULT_EXTRA_CONFIG) -> Tuple[dict, DiGraph]:
 | |
|     """Loads equipment configuration and network topology from JSON data.
 | |
| 
 | |
|     :param eqpt: Dictionary containing the equipment configuration in JSON format.
 | |
|         It includes details about the devices to be processed and structured.
 | |
|     :type eqpt: dict
 | |
|     :param topology: Dictionary representing the network topology in JSON format,
 | |
|         defining the structure of the network and its connections.
 | |
|     :type topology: dict
 | |
|     :param extra_equipments: dictionary containing additional libraries (eg for pluggables). Key can be
 | |
|                              the file Path or any other string.
 | |
|     :type extra_equipments: Optional[Dict[str, Dict]]
 | |
|     :param extra_configs: Additional configurations for amplifiers in the library
 | |
|     :type extra_configs: Dict[str, Dict]
 | |
| 
 | |
|     :return: A tuple containing:
 | |
| 
 | |
|         - A dictionary with the processed equipment configuration.
 | |
|         - A directed graph (DiGraph) representing the network topology, where nodes
 | |
|           correspond to equipment and edges define their connections.
 | |
|     """
 | |
|     equipment = _equipment_from_json(eqpt, extra_configs)
 | |
|     if extra_equipments:
 | |
|         merge_equipment(equipment, extra_equipments, extra_configs)
 | |
|     network = network_from_json(topology, equipment)
 | |
|     return equipment, network
 |