mirror of
https://github.com/Telecominfraproject/oopt-gnpy.git
synced 2025-11-01 10:38:10 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ef01d54a5 | ||
|
|
4b50ee0c2d | ||
|
|
33a289e22b | ||
|
|
e593b8c9ec | ||
|
|
94a6f922cd | ||
|
|
fbe387915b | ||
|
|
fce9d1d293 | ||
|
|
a59db8fd12 | ||
|
|
de509139b3 | ||
|
|
bb77b3f4a8 | ||
|
|
34c7fd1b60 | ||
|
|
89a962ffaf | ||
|
|
1722fbec13 | ||
|
|
e48aa57c35 | ||
|
|
e3e37b1986 | ||
|
|
abf42afaf8 | ||
|
|
310995045e | ||
|
|
c840bb1a44 | ||
|
|
4b6f4af3a5 | ||
|
|
dc68d38293 | ||
|
|
defe3bee5c | ||
|
|
32adc0fc53 | ||
|
|
4796266657 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -118,10 +118,10 @@ jobs:
|
||||
python_version: "3.11"
|
||||
- os: windows-2022
|
||||
python_version: "3.12"
|
||||
- os: macos-12
|
||||
python_version: "3.11"
|
||||
- os: macos-13
|
||||
python_version: "3.12"
|
||||
- os: macos-14
|
||||
python_version: "3.12"
|
||||
|
||||
paywalled-platforms:
|
||||
name: Tests on paywalled platforms
|
||||
|
||||
@@ -114,10 +114,6 @@ and a fiber span from node3 to node6::
|
||||
If filled they must contain strings with the same constraint as "City" names. Its value is used to differenate links having the same end points. In this case different Id should be used. Cable Ids are not meant to be unique in general.
|
||||
|
||||
|
||||
|
||||
|
||||
(in progress)
|
||||
|
||||
.. _excel-equipment-sheet:
|
||||
|
||||
Eqpt sheet
|
||||
@@ -192,7 +188,42 @@ This generates a text file meshTopologyExampleV2_eqt_sheet.txt whose content ca
|
||||
|
||||
- **delta_p**, in dBm, is not mandatory. If filled it is used to set the output target power per channel at the output of the amplifier, if power_mode is True. The output power is then set to power_dbm + delta_power.
|
||||
|
||||
# to be completed #
|
||||
|
||||
.. _excel-roadms-sheet:
|
||||
|
||||
Roadms sheet
|
||||
------------
|
||||
|
||||
The ROADM sheet (named "Roadms") is optional.
|
||||
If provided, it can be used to specify:
|
||||
|
||||
- per channel power target on a specific ROADM degree (*per_degree_pch_out_db*),
|
||||
- ROADM type variety,
|
||||
- impairment ID (identifier) on a particular ROADM path (from degree - to degree).
|
||||
|
||||
This sheet contains six columns:
|
||||
|
||||
Node A ; Node Z ; per degree target power (dBm) ; type_variety ; from degrees ; from degree to degree impairment id
|
||||
|
||||
- **Node A** is mandatory. Name of the ROADM node (as listed in Nodes sheet).
|
||||
Must be a 'ROADM' (Type attribute in Node sheet), its number of occurence may be equal to its degree.
|
||||
|
||||
- **Node Z** is mandatory. Egress direction from the *Node A* ROADM site. Multiple Links between the same Node A
|
||||
and NodeZ is not supported.
|
||||
|
||||
- **per degree target power (dBm)** (optional).
|
||||
If filled it must contain a value in dBm corresponding to :ref:`per_degree_pch_out_db<roadm_json_instance>` on the **Node Z** degree.
|
||||
Defaults to equipment library value if not filled.
|
||||
|
||||
- **type_variety** (optional). Must be the same for all ROADM entries if filled,
|
||||
and defined in the :ref:`equipment library<roadm>`. Defaults to 'default' if not filled.
|
||||
|
||||
- **from degrees** (optional): List of Node names separated by ' | '. Names must be present in Node sheet.
|
||||
Together with Node Z, they define a list of internal path in ROADM for which the impairment ID applies
|
||||
|
||||
- **from degree to degree impairment id** (optional):List of impairment IDs separated by ' | '. Must be filled
|
||||
if **from degrees** is defined.
|
||||
The impairment ID must be defined in the equipment library and be of "express" type.
|
||||
|
||||
(in progress)
|
||||
|
||||
|
||||
@@ -1280,6 +1280,8 @@ allowed ``amplifiers`` list defined in the library.
|
||||
Roadm
|
||||
~~~~~
|
||||
|
||||
.. _roadm_json_instance:
|
||||
|
||||
+----------------------------------------+-----------+----------------------------------------------------+
|
||||
| field | type | description |
|
||||
+========================================+===========+====================================================+
|
||||
|
||||
@@ -331,7 +331,7 @@ class Roadm(_Node):
|
||||
if self.per_degree_pch_psw:
|
||||
to_json['params']['per_degree_psd_out_mWperSlotWidth'] = self.per_degree_pch_psw
|
||||
if self.per_degree_impairments:
|
||||
to_json['per_degree_impairments'] = list(self.per_degree_impairments.values())
|
||||
to_json['params']['per_degree_impairments'] = list(self.per_degree_impairments.values())
|
||||
|
||||
if self.params.design_bands is not None:
|
||||
if len(self.params.design_bands) > 1:
|
||||
@@ -917,7 +917,10 @@ class Edfa(_Node):
|
||||
if operational is None:
|
||||
operational = {}
|
||||
self.variety_list = kwargs.pop('variety_list', None)
|
||||
super().__init__(*args, params=EdfaParams(**params), operational=EdfaOperational(**operational), **kwargs)
|
||||
try:
|
||||
super().__init__(*args, params=EdfaParams(**params), operational=EdfaOperational(**operational), **kwargs)
|
||||
except ParametersError as e:
|
||||
raise ParametersError(f'{kwargs["uid"]}: {e}') from e
|
||||
self.interpol_dgt = None # interpolated dynamic gain tilt defined per frequency on amp band
|
||||
self.interpol_gain_ripple = None # gain ripple
|
||||
self.interpol_nf_ripple = None # nf_ripple
|
||||
@@ -985,7 +988,8 @@ class Edfa(_Node):
|
||||
f' type_variety: {self.params.type_variety}',
|
||||
f' effective gain(dB): {self.effective_gain:.2f}',
|
||||
' (before att_in and before output VOA)',
|
||||
f' tilt-target(dB) {self.tilt_target:.2f}',
|
||||
f' tilt-target(dB) {self.tilt_target if self.tilt_target else 0:.2f}',
|
||||
# avoids -0.00 value for tilt_target
|
||||
f' noise figure (dB): {nf:.2f}',
|
||||
f' (including att_in)',
|
||||
f' pad att_in (dB): {self.att_in:.2f}',
|
||||
@@ -1317,7 +1321,7 @@ class Multiband_amplifier(_Node):
|
||||
try:
|
||||
super().__init__(params=MultiBandParams(**params), **kwargs)
|
||||
except ParametersError as e:
|
||||
raise ParametersError(f'{kwargs["uid"]}: {e}')
|
||||
raise ParametersError(f'{kwargs["uid"]}: {e}') from e
|
||||
self.amplifiers = {}
|
||||
if 'type_variety' in kwargs:
|
||||
kwargs.pop('type_variety')
|
||||
|
||||
@@ -85,7 +85,7 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
|
||||
return trx_params
|
||||
|
||||
|
||||
def find_type_variety(amps: List[str], equipment: dict) -> str:
|
||||
def find_type_variety(amps: List[str], equipment: dict) -> List[str]:
|
||||
"""Returns the multiband type_variety associated with a list of single band type_varieties
|
||||
Args:
|
||||
amps (List[str]): A list of single band type_varieties.
|
||||
@@ -106,7 +106,7 @@ def find_type_variety(amps: List[str], equipment: dict) -> str:
|
||||
if not _found_type:
|
||||
msg = f'{amps} amps do not belong to the same amp type {listes}'
|
||||
raise ConfigurationError(msg)
|
||||
return _found_type[0]
|
||||
return _found_type
|
||||
|
||||
|
||||
def find_type_varieties(amps: List[str], equipment: dict) -> List[List[str]]:
|
||||
|
||||
@@ -11,7 +11,7 @@ This module contains classes for modelling :class:`SpectralInformation`.
|
||||
from __future__ import annotations
|
||||
from collections import namedtuple
|
||||
from collections.abc import Iterable
|
||||
from typing import Union, List
|
||||
from typing import Union, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from numpy import argsort, mean, array, append, ones, ceil, any, zeros, outer, full, ndarray, asarray
|
||||
|
||||
@@ -325,7 +325,7 @@ def is_in_band(frequency: float, band: dict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def demuxed_spectral_information(input_si: SpectralInformation, band: dict) -> SpectralInformation:
|
||||
def demuxed_spectral_information(input_si: SpectralInformation, band: dict) -> Optional[SpectralInformation]:
|
||||
"""extract a si based on band
|
||||
"""
|
||||
filtered_indices = [i for i, f in enumerate(input_si.frequency)
|
||||
|
||||
@@ -118,7 +118,7 @@ def select_edfa(raman_allowed: bool, gain_target: float, power_target: float, ed
|
||||
tilt_target = 0
|
||||
with warnings.catch_warnings(record=True) as caught_warnings:
|
||||
acceptable_power_list = \
|
||||
filter_edfa_list_based_on_targets(edfa_eqpt, power_target, gain_target,
|
||||
filter_edfa_list_based_on_targets(uid, edfa_eqpt, power_target, gain_target,
|
||||
tilt_target, target_extended_gain,
|
||||
raman_allowed, verbose)
|
||||
if caught_warnings:
|
||||
@@ -247,8 +247,7 @@ def estimate_raman_gain(node, equipment, power_dbm):
|
||||
node.estimated_gain = estimated_gain
|
||||
SimParams.set_params(save_sim_params)
|
||||
return round(estimated_gain, 2)
|
||||
else:
|
||||
return 0.0
|
||||
return 0.0
|
||||
|
||||
|
||||
def span_loss(network, node, equipment, input_power=None):
|
||||
@@ -369,7 +368,7 @@ def compute_band_power_deviation_and_tilt(srs_power_deviation, design_bands: dic
|
||||
return deviation_db, tilt_target
|
||||
|
||||
|
||||
def compute_tilt_using_previous_and_next_spans(prev_node, next_node, design_bands: List[str],
|
||||
def compute_tilt_using_previous_and_next_spans(prev_node, next_node, design_bands: Dict[str, float],
|
||||
input_powers: Dict[str, float], equipment: dict, network: DiGraph, prev_weight: float = 1.0,
|
||||
next_weight: float = 0) -> Tuple[Dict[str, float], Dict[str, float]]:
|
||||
"""Compute the power deviation per band and the tilt target based on previous and next spans.
|
||||
@@ -557,7 +556,7 @@ def compute_gain_power_and_tilt_target(node: elements.Edfa, prev_node, next_node
|
||||
return gain_target, power_target, _tilt_target, dp, voa, node_loss
|
||||
|
||||
|
||||
def filter_edfa_list_based_on_targets(edfa_eqpt: dict, power_target: float, gain_target: float,
|
||||
def filter_edfa_list_based_on_targets(uid: str, edfa_eqpt: dict, power_target: float, gain_target: float,
|
||||
tilt_target: float, target_extended_gain: float,
|
||||
raman_allowed: bool = True, verbose: bool = False):
|
||||
"""Filter the amplifiers based on power, gain, and tilt targets.
|
||||
@@ -626,7 +625,7 @@ def filter_edfa_list_based_on_targets(edfa_eqpt: dict, power_target: float, gain
|
||||
please increase span fiber padding')
|
||||
else:
|
||||
if verbose:
|
||||
logger.warning('\n\tWARNING: target gain is below all available amplifiers min gain: '
|
||||
logger.warning(f'\n\tWARNING: target gain in node {uid} is below all available amplifiers min gain: '
|
||||
+ '\n\tamplifier input padding will be assumed, consider increase span fiber padding '
|
||||
+ 'instead.\n')
|
||||
acceptable_gain_min_list = edfa_list
|
||||
@@ -646,7 +645,7 @@ def filter_edfa_list_based_on_targets(edfa_eqpt: dict, power_target: float, gain
|
||||
return acceptable_power_list
|
||||
|
||||
|
||||
def preselect_multiband_amps(_amplifiers: dict, prev_node, next_node, power_mode: bool, prev_voa: dict, prev_dp: dict,
|
||||
def preselect_multiband_amps(uid: str, _amplifiers: dict, prev_node, next_node, power_mode: bool, prev_voa: dict, prev_dp: dict,
|
||||
pref_total_db: float, network: DiGraph, equipment: dict, restrictions: List,
|
||||
_design_bands: dict, deviation_db: dict, tilt_target: dict):
|
||||
"""Preselect multiband amplifiers that are eligible with respect to power, gain and tilt target
|
||||
@@ -691,7 +690,7 @@ def preselect_multiband_amps(_amplifiers: dict, prev_node, next_node, power_mode
|
||||
compute_gain_power_and_tilt_target(amp, prev_node, next_node, power_mode, prev_voa[band], prev_dp[band],
|
||||
pref_total_db, network, equipment, deviation_db[band], tilt_target[band])
|
||||
_selection = [a.variety
|
||||
for a in filter_edfa_list_based_on_targets(edfa_eqpt, power_target, gain_target, _tilt_target,
|
||||
for a in filter_edfa_list_based_on_targets(uid, edfa_eqpt, power_target, gain_target, _tilt_target,
|
||||
target_extended_gain)]
|
||||
listes = find_type_varieties(_selection, equipment)
|
||||
_selected_type_varieties = []
|
||||
@@ -780,7 +779,12 @@ def set_one_amplifier(node: elements.Edfa, prev_node, next_node, power_mode: boo
|
||||
+ '\tmax flat gain: '
|
||||
+ f'{equipment["Edfa"][node.params.type_variety].gain_flatmax}dB ; '
|
||||
+ f'required gain: {round(gain_target, 2)}dB. Please check amplifier type.\n')
|
||||
|
||||
if gain_target - equipment['Edfa'][node.params.type_variety].gain_min < 0 and verbose:
|
||||
logger.warning(f'\n\tWARNING: effective gain in Node {node.uid}\n'
|
||||
+ f'\tis below user specified amplifier {node.params.type_variety}\n'
|
||||
+ '\tmin gain: '
|
||||
+ f'{equipment["Edfa"][node.params.type_variety].gain_min}dB ; '
|
||||
+ f'required gain: {round(gain_target, 2)}dB. Please check amplifier type.\n')
|
||||
node.delta_p = dp if power_mode else None
|
||||
node.effective_gain = gain_target
|
||||
node.tilt_target = _tilt_target
|
||||
@@ -942,7 +946,7 @@ def set_egress_amplifier(network: DiGraph, this_node: Union[elements.Roadm, elem
|
||||
# only select amplifiers which match the design bands
|
||||
restrictions_multi = get_node_restrictions(node, prev_node, next_node, equipment, _design_bands)
|
||||
restrictions_edfa = \
|
||||
preselect_multiband_amps(node.amplifiers, prev_node, next_node, power_mode,
|
||||
preselect_multiband_amps(node.uid, node.amplifiers, prev_node, next_node, power_mode,
|
||||
prev_voa, prev_dp, pref_total_db,
|
||||
network, equipment, restrictions_multi, _design_bands,
|
||||
deviation_db=deviation_db, tilt_target=tilt_target)
|
||||
@@ -957,7 +961,7 @@ def set_egress_amplifier(network: DiGraph, this_node: Union[elements.Roadm, elem
|
||||
deviation_db=deviation_db[band_name], tilt_target=tilt_target[band_name])
|
||||
amps_type_varieties = [a.type_variety for a in node.amplifiers.values()]
|
||||
try:
|
||||
node.type_variety = find_type_variety(amps_type_varieties, equipment)
|
||||
node.type_variety = find_type_variety(amps_type_varieties, equipment)[0]
|
||||
except ConfigurationError as e:
|
||||
# should never come here... only for debugging
|
||||
msg = f'In {node.uid}: {e}'
|
||||
|
||||
@@ -12,7 +12,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
|
||||
from typing import List, Union
|
||||
|
||||
from gnpy.core.exceptions import ConfigurationError
|
||||
|
||||
@@ -444,6 +444,16 @@ def restore_order(elements, order):
|
||||
return [elements[i[0]] for i in sorted(enumerate(order), key=lambda x:x[1]) if elements[i[0]] is not None]
|
||||
|
||||
|
||||
def unique_ordered(elements):
|
||||
"""
|
||||
"""
|
||||
unique_elements = []
|
||||
for element in elements:
|
||||
if element not in unique_elements:
|
||||
unique_elements.append(element)
|
||||
return unique_elements
|
||||
|
||||
|
||||
def calculate_absolute_min_or_zero(x: array) -> array:
|
||||
"""Calculates the element-wise absolute minimum between the x and zero.
|
||||
|
||||
@@ -525,3 +535,26 @@ def find_common_range(amp_bands: List[List[dict]], default_band_f_min: float, de
|
||||
for first in common_range for second in bands
|
||||
if max(first['f_min'], second['f_min']) < min(first['f_max'], second['f_max'])]
|
||||
return sorted(common_range, key=lambda x: x['f_min'])
|
||||
|
||||
|
||||
def transform_data(data: str) -> Union[List[int], None]:
|
||||
"""Transforms a float into an list of one integer or a string separated by "|" into a list of integers.
|
||||
|
||||
Args:
|
||||
data (float or str): The data to transform.
|
||||
|
||||
Returns:
|
||||
list of int: The transformed data as a list of integers.
|
||||
|
||||
Examples:
|
||||
>>> transform_data(5.0)
|
||||
[5]
|
||||
|
||||
>>> transform_data('1 | 2 | 3')
|
||||
[1, 2, 3]
|
||||
"""
|
||||
if isinstance(data, float):
|
||||
return [int(data)]
|
||||
if isinstance(data, str):
|
||||
return [int(x) for x in data.split(' | ')]
|
||||
return None
|
||||
|
||||
@@ -11,14 +11,13 @@ Common code for CLI examples
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from math import ceil
|
||||
from numpy import mean
|
||||
from pathlib import Path
|
||||
from copy import deepcopy
|
||||
|
||||
import gnpy.core.ansi_escapes as ansi_escapes
|
||||
from gnpy.core.elements import Transceiver, Fiber, RamanFiber, Roadm
|
||||
import gnpy.core.exceptions as exceptions
|
||||
from gnpy.core import ansi_escapes
|
||||
from gnpy.core.elements import Transceiver, Fiber, RamanFiber
|
||||
from gnpy.core import exceptions
|
||||
from gnpy.core.parameters import SimParams
|
||||
from gnpy.core.utils import lin2db, pretty_summary_print, per_label_average, watt2dbm
|
||||
from gnpy.topology.request import (ResultElement, jsontocsv, BLOCKING_NOPATH)
|
||||
@@ -106,11 +105,14 @@ def _add_common_options(parser: argparse.ArgumentParser, network_default: Path):
|
||||
|
||||
|
||||
def transmission_main_example(args=None):
|
||||
"""Main script running a single simulation. It returns the detailed power across crossed elements and
|
||||
average performance accross all channels.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Send a full spectrum load through the network from point A to point B',
|
||||
epilog=_help_footer,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
)
|
||||
_add_common_options(parser, network_default=_examples_dir / 'edfa_example_network.json')
|
||||
parser.add_argument('--show-channels', action='store_true', help='Show final per-channel OSNR and GSNR summary')
|
||||
parser.add_argument('-pl', '--plot', action='store_true')
|
||||
@@ -144,14 +146,14 @@ def transmission_main_example(args=None):
|
||||
source = None
|
||||
if args.source:
|
||||
source = transceivers.pop(args.source, None)
|
||||
valid_source = True if source else False
|
||||
valid_source = bool(source)
|
||||
|
||||
destination = None
|
||||
nodes_list = []
|
||||
loose_list = []
|
||||
if args.destination:
|
||||
destination = transceivers.pop(args.destination, None)
|
||||
valid_destination = True if destination else False
|
||||
valid_destination = bool(destination)
|
||||
|
||||
# If no exact match try to find partial match
|
||||
if args.source and not source:
|
||||
@@ -209,8 +211,8 @@ def transmission_main_example(args=None):
|
||||
except ValueError:
|
||||
sys.exit(1)
|
||||
# print or export results
|
||||
spans = [s.params.length for s in path if isinstance(s, RamanFiber) or isinstance(s, Fiber)]
|
||||
print(f'\nThere are {len(spans)} fiber spans over {sum(spans)/1000:.0f} km between {source.uid} '
|
||||
spans = [s.params.length for s in path if isinstance(s, (Fiber, RamanFiber))]
|
||||
print(f'\nThere are {len(spans)} fiber spans over {sum(spans) / 1000:.0f} km between {source.uid} '
|
||||
f'and {destination.uid}')
|
||||
print(f'\nNow propagating between {source.uid} and {destination.uid}:')
|
||||
print(f'Reference used for design: (Input optical power reference in span = {watt2dbm(ref_req.power):.2f}dBm,\n'
|
||||
@@ -223,22 +225,22 @@ def transmission_main_example(args=None):
|
||||
+ ' transceiver output power = '
|
||||
+ f'{pretty_summary_print(per_label_average(watt2dbm(infos.tx_power), infos.label))}dBm,\n'
|
||||
+ f' nb_channels = {infos.number_of_channels})')
|
||||
for path, power_dbm in zip(propagations_for_path, powers_dbm):
|
||||
for mypath, power_dbm in zip(propagations_for_path, powers_dbm):
|
||||
if power_mode:
|
||||
print(f'Input optical power reference in span = {ansi_escapes.cyan}{power_dbm:.2f} '
|
||||
+ f'dBm{ansi_escapes.reset}:')
|
||||
else:
|
||||
print('\nPropagating in {ansi_escapes.cyan}gain mode{ansi_escapes.reset}: power cannot be set manually')
|
||||
if len(powers_dbm) == 1:
|
||||
for elem in path:
|
||||
for elem in mypath:
|
||||
print(elem)
|
||||
if power_mode:
|
||||
print(f'\nTransmission result for input optical power reference in span = {power_dbm:.2f} dBm:')
|
||||
else:
|
||||
print(f'\nTransmission results:')
|
||||
print('\nTransmission results:')
|
||||
print(f' Final GSNR (0.1 nm): {ansi_escapes.cyan}{mean(destination.snr_01nm):.02f} dB{ansi_escapes.reset}')
|
||||
else:
|
||||
print(path[-1])
|
||||
print(mypath[-1])
|
||||
|
||||
if args.save_network is not None:
|
||||
save_network(network, args.save_network)
|
||||
@@ -286,11 +288,14 @@ def _path_result_json(pathresult):
|
||||
|
||||
|
||||
def path_requests_run(args=None):
|
||||
"""Main script running several services simulations. It returns a summary of the average performance
|
||||
for each service.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Compute performance for a list of services provided in a json file or an excel sheet',
|
||||
epilog=_help_footer,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
)
|
||||
_add_common_options(parser, network_default=_examples_dir / 'meshTopologyExampleV2.xls')
|
||||
parser.add_argument('service_filename', nargs='?', type=Path, metavar='SERVICES-REQUESTS.(json|xls|xlsx)',
|
||||
default=_examples_dir / 'meshTopologyExampleV2.xls',
|
||||
@@ -302,7 +307,6 @@ def path_requests_run(args=None):
|
||||
parser.add_argument('--redesign-per-request', action='store_true', help='Redesign the network at each request'
|
||||
+ ' computation using the request as the reference channel')
|
||||
|
||||
|
||||
args = parser.parse_args(args if args is not None else sys.argv[1:])
|
||||
_setup_logging(args)
|
||||
|
||||
@@ -312,18 +316,13 @@ def path_requests_run(args=None):
|
||||
load_common_data(args.equipment, args.topology, args.sim_params, args.save_network_before_autodesign)
|
||||
|
||||
# Build the network once using the default power defined in SI in eqpt config
|
||||
# TODO power density: db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
|
||||
# spacing, f_min and f_max
|
||||
if args.save_network is not None:
|
||||
save_network(network, args.save_network)
|
||||
print(f'Network (after autodesign) saved to {args.save_network}')
|
||||
|
||||
try:
|
||||
network, _, _ = designed_network(equipment, network, no_insert_edfas=args.no_insert_edfas)
|
||||
data = load_requests(args.service_filename, equipment, bidir=args.bidir,
|
||||
network=network, network_filename=args.topology)
|
||||
_data = requests_from_json(data, equipment)
|
||||
oms_list, propagatedpths, reversed_propagatedpths, rqs, dsjn, result = \
|
||||
_, propagatedpths, reversed_propagatedpths, rqs, dsjn, result = \
|
||||
planning(network, equipment, data, redesign=args.redesign_per_request)
|
||||
except exceptions.NetworkTopologyError as e:
|
||||
print(f'{ansi_escapes.red}Invalid network definition:{ansi_escapes.reset} {e}')
|
||||
@@ -344,6 +343,10 @@ def path_requests_run(args=None):
|
||||
print(f'{ansi_escapes.blue}The following services have been requested:{ansi_escapes.reset}')
|
||||
print(_data)
|
||||
|
||||
if args.save_network is not None:
|
||||
save_network(network, args.save_network)
|
||||
print(f'Network (after autodesign) saved to {args.save_network}')
|
||||
|
||||
print(f'{ansi_escapes.blue}Result summary{ansi_escapes.reset}')
|
||||
header = ['req id', ' demand', ' GSNR@bandwidth A-Z (Z-A)', ' GSNR@0.1nm A-Z (Z-A)',
|
||||
' Receiver minOSNR', ' mode', ' Gbit/s', ' nb of tsp pairs',
|
||||
@@ -353,27 +356,27 @@ def path_requests_run(args=None):
|
||||
for i, this_p in enumerate(propagatedpths):
|
||||
rev_pth = reversed_propagatedpths[i]
|
||||
if rev_pth and this_p:
|
||||
psnrb = f'{round(mean(this_p[-1].snr),2)} ({round(mean(rev_pth[-1].snr),2)})'
|
||||
psnrb = f'{round(mean(this_p[-1].snr), 2)} ({round(mean(rev_pth[-1].snr), 2)})'
|
||||
psnr = f'{round(mean(this_p[-1].snr_01nm), 2)}' +\
|
||||
f' ({round(mean(rev_pth[-1].snr_01nm),2)})'
|
||||
f' ({round(mean(rev_pth[-1].snr_01nm), 2)})'
|
||||
elif this_p:
|
||||
psnrb = f'{round(mean(this_p[-1].snr),2)}'
|
||||
psnr = f'{round(mean(this_p[-1].snr_01nm),2)}'
|
||||
psnrb = f'{round(mean(this_p[-1].snr), 2)}'
|
||||
psnr = f'{round(mean(this_p[-1].snr_01nm), 2)}'
|
||||
|
||||
try:
|
||||
if rqs[i].blocking_reason in BLOCKING_NOPATH:
|
||||
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} :',
|
||||
f'-', f'-', f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',
|
||||
f'-', f'{rqs[i].blocking_reason}']
|
||||
'-', '-', '-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9, 2)}',
|
||||
'-', '{rqs[i].blocking_reason}']
|
||||
else:
|
||||
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,
|
||||
psnr, f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9, 2)}',
|
||||
f'-', f'{rqs[i].blocking_reason}']
|
||||
psnr, '-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9, 2)}',
|
||||
'-', f'{rqs[i].blocking_reason}']
|
||||
except AttributeError:
|
||||
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,
|
||||
psnr, f'{rqs[i].OSNR + equipment["SI"]["default"].sys_margins}',
|
||||
f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',
|
||||
f'{ceil(rqs[i].path_bandwidth / rqs[i].bit_rate) }', f'({rqs[i].N},{rqs[i].M})']
|
||||
f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9, 2)}',
|
||||
f'{ceil(rqs[i].path_bandwidth / rqs[i].bit_rate)}', f'({rqs[i].N},{rqs[i].M})']
|
||||
data.append(line)
|
||||
|
||||
col_width = max(len(word) for row in data for word in row[2:]) # padding
|
||||
|
||||
@@ -20,7 +20,6 @@ In the "Links" sheet, only the first three columns ("Node A", "Node Z" and
|
||||
the "east" information so that it is possible to input undirected data.
|
||||
"""
|
||||
|
||||
from xlrd import open_workbook
|
||||
from logging import getLogger
|
||||
from argparse import ArgumentParser
|
||||
from collections import namedtuple, Counter, defaultdict
|
||||
@@ -28,8 +27,12 @@ from itertools import chain
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from copy import copy
|
||||
from typing import Dict, List, Tuple, DefaultDict
|
||||
from xlrd import open_workbook
|
||||
from xlrd.biffh import XLRDError
|
||||
from networkx import DiGraph
|
||||
|
||||
from gnpy.core.utils import silent_remove
|
||||
from gnpy.core.utils import silent_remove, transform_data
|
||||
from gnpy.core.exceptions import NetworkTopologyError
|
||||
from gnpy.core.elements import Edfa, Fused, Fiber
|
||||
|
||||
@@ -38,12 +41,16 @@ _logger = getLogger(__name__)
|
||||
|
||||
|
||||
def all_rows(sh, start=0):
|
||||
"""Returns all rows of the xls(x) sheet starting from start row
|
||||
"""
|
||||
return (sh.row(x) for x in range(start, sh.nrows))
|
||||
|
||||
|
||||
class Node(object):
|
||||
class Node:
|
||||
"""Node data class
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(Node, self).__init__()
|
||||
super().__init__()
|
||||
self.update_attr(kwargs)
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
@@ -65,13 +72,13 @@ class Node(object):
|
||||
}
|
||||
|
||||
|
||||
class Link(object):
|
||||
class Link:
|
||||
"""attribtes from west parse_ept_headers dict
|
||||
+node_a, node_z, west_fiber_con_in, east_fiber_con_in
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Link, self).__init__()
|
||||
super().__init__()
|
||||
self.update_attr(kwargs)
|
||||
self.distance_units = 'km'
|
||||
|
||||
@@ -80,7 +87,7 @@ class Link(object):
|
||||
for k, v in self.default_values.items():
|
||||
v = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v)
|
||||
k = 'west' + k.split('east')[-1]
|
||||
k = 'west' + k.rsplit('east', maxsplit=1)[-1]
|
||||
v = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v)
|
||||
|
||||
@@ -101,9 +108,11 @@ class Link(object):
|
||||
}
|
||||
|
||||
|
||||
class Eqpt(object):
|
||||
class Eqpt:
|
||||
"""
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(Eqpt, self).__init__()
|
||||
super().__init__()
|
||||
self.update_attr(kwargs)
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
@@ -111,7 +120,7 @@ class Eqpt(object):
|
||||
for k, v in self.default_values.items():
|
||||
v_east = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v_east)
|
||||
k = 'west' + k.split('east')[-1]
|
||||
k = 'west' + k.rsplit('east', maxsplit=1)[-1]
|
||||
v_west = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v_west)
|
||||
|
||||
@@ -119,17 +128,18 @@ class Eqpt(object):
|
||||
'from_city': '',
|
||||
'to_city': '',
|
||||
'east_amp_type': '',
|
||||
'east_att_in': 0,
|
||||
'east_amp_gain': None,
|
||||
'east_amp_dp': None,
|
||||
'east_tilt_vs_wavelength': 0,
|
||||
'east_tilt_vs_wavelength': None,
|
||||
'east_att_out': None
|
||||
}
|
||||
|
||||
|
||||
class Roadm(object):
|
||||
class Roadm:
|
||||
"""
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(Roadm, self).__init__()
|
||||
super().__init__()
|
||||
self.update_attr(kwargs)
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
@@ -140,7 +150,10 @@ class Roadm(object):
|
||||
|
||||
default_values = {'from_node': '',
|
||||
'to_node': '',
|
||||
'target_pch_out_db': None
|
||||
'target_pch_out_db': None,
|
||||
'type_variety': None,
|
||||
'from_degrees': None,
|
||||
'impairment_ids': None
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +166,7 @@ def read_header(my_sheet, line, slice_):
|
||||
try:
|
||||
header = [x.value.strip() for x in my_sheet.row_slice(line, slice_[0], slice_[1])]
|
||||
header_i = [Param_header(header, i + slice_[0]) for i, header in enumerate(header) if header != '']
|
||||
except Exception:
|
||||
except (AttributeError, IndexError):
|
||||
header_i = []
|
||||
if header_i != [] and header_i[-1].colindex != slice_[1]:
|
||||
header_i.append(Param_header('', slice_[1]))
|
||||
@@ -169,7 +182,7 @@ def read_slice(my_sheet, line, slice_, header):
|
||||
try:
|
||||
slice_range = next((h.colindex, header_i[i + 1].colindex)
|
||||
for i, h in enumerate(header_i) if header in h.header)
|
||||
except Exception:
|
||||
except StopIteration:
|
||||
pass
|
||||
return slice_range
|
||||
|
||||
@@ -190,8 +203,7 @@ def parse_headers(my_sheet, input_headers_dict, headers, start_line, slice_in):
|
||||
msg = f'missing header {h0}'
|
||||
if h0 in ('east', 'Node A', 'Node Z', 'City'):
|
||||
raise NetworkTopologyError(msg)
|
||||
else:
|
||||
_logger.warning(msg)
|
||||
_logger.warning(msg)
|
||||
elif not isinstance(input_headers_dict[h0], dict):
|
||||
headers[slice_out[0]] = input_headers_dict[h0]
|
||||
else:
|
||||
@@ -203,22 +215,33 @@ def parse_headers(my_sheet, input_headers_dict, headers, start_line, slice_in):
|
||||
|
||||
|
||||
def parse_row(row, headers):
|
||||
"""
|
||||
"""
|
||||
return {f: r.value for f, r in
|
||||
zip([label for label in headers.values()], [row[i] for i in headers])}
|
||||
zip(list(headers.values()), [row[i] for i in headers])}
|
||||
|
||||
|
||||
def parse_sheet(my_sheet, input_headers_dict, header_line, start_line, column):
|
||||
"""
|
||||
"""
|
||||
headers = parse_headers(my_sheet, input_headers_dict, {}, header_line, (0, column))
|
||||
for row in all_rows(my_sheet, start=start_line):
|
||||
yield parse_row(row[0: column], headers)
|
||||
|
||||
|
||||
def _format_items(items):
|
||||
def _format_items(items: List[str]):
|
||||
"""formating utils
|
||||
"""
|
||||
return '\n'.join(f' - {item}' for item in items)
|
||||
|
||||
|
||||
def sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city):
|
||||
|
||||
def sanity_check(nodes: List[Node], links: List[Link],
|
||||
nodes_by_city: Dict[str, Node], links_by_city: DefaultDict[str, List[Link]],
|
||||
eqpts_by_city: DefaultDict[str, List[Eqpt]]) -> Tuple[List[Node], List[Link]]:
|
||||
"""Raise correct issues if xls(x) is not correct, Correct type to ROADM if more tha 2-degrees
|
||||
checks duplicate links, unreferenced nodes in links, in eqpts, unreferenced link in eqpts,
|
||||
duplicate items
|
||||
"""
|
||||
duplicate_links = []
|
||||
for l1 in links:
|
||||
for l2 in links:
|
||||
@@ -227,6 +250,7 @@ def sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city):
|
||||
link {l1.from_city}-{l1.to_city} is duplicate \
|
||||
\nthe 1st duplicate link will be removed but you should check Links sheet input')
|
||||
duplicate_links.append(l1)
|
||||
|
||||
if duplicate_links:
|
||||
msg = 'XLS error: ' \
|
||||
+ f'links {_format_items([(d.from_city, d.to_city) for d in duplicate_links])} are duplicate'
|
||||
@@ -315,13 +339,29 @@ def create_roadm_element(node, roadms_by_city):
|
||||
'booster_variety_list': silent_remove(node.booster_restriction.split(' | '), '')}
|
||||
}
|
||||
if node.city in roadms_by_city.keys():
|
||||
if 'params' not in roadm.keys():
|
||||
if 'params' not in roadm:
|
||||
roadm['params'] = {}
|
||||
roadm['params']['per_degree_pch_out_db'] = {}
|
||||
for elem in roadms_by_city[node.city]:
|
||||
to_node = f'east edfa in {node.city} to {elem.to_node}'
|
||||
if elem.target_pch_out_db is not None:
|
||||
roadm['params']['per_degree_pch_out_db'][to_node] = elem.target_pch_out_db
|
||||
if elem.from_degrees is not None and elem.impairment_ids is not None:
|
||||
# only set per degree impairment if there is an entry (reduce verbose)
|
||||
if roadm['params'].get('per_degree_impairments') is None:
|
||||
roadm['params']['per_degree_impairments'] = []
|
||||
fromdegrees = elem.from_degrees.split(' | ')
|
||||
impairment_ids = transform_data(elem.impairment_ids)
|
||||
if len(fromdegrees) != len(impairment_ids):
|
||||
msg = f'Roadm {node.city} per degree impairment id do not match with from degree definition'
|
||||
raise NetworkTopologyError(msg)
|
||||
for from_degree, impairment_id in zip(fromdegrees, impairment_ids):
|
||||
from_node = f'west edfa in {node.city} to {from_degree}'
|
||||
roadm['params']['per_degree_impairments'].append({'from_degree': from_node,
|
||||
'to_degree': to_node,
|
||||
'impairment_id': impairment_id})
|
||||
if elem.type_variety is not None:
|
||||
roadm['type_variety'] = elem.type_variety
|
||||
roadm['metadata'] = {'location': {'city': node.city,
|
||||
'region': node.region,
|
||||
'latitude': node.latitude,
|
||||
@@ -330,7 +370,7 @@ def create_roadm_element(node, roadms_by_city):
|
||||
return roadm
|
||||
|
||||
|
||||
def create_east_eqpt_element(node):
|
||||
def create_east_eqpt_element(node: Node, nodes_by_city: Dict[str, Node]) -> dict:
|
||||
""" create amplifiers json elements for the east direction.
|
||||
this includes the case where the case of a fused element defined instead of an
|
||||
ILA in eqpt sheet
|
||||
@@ -363,7 +403,7 @@ def create_east_eqpt_element(node):
|
||||
return eqpt
|
||||
|
||||
|
||||
def create_west_eqpt_element(node):
|
||||
def create_west_eqpt_element(node: Node, nodes_by_city: Dict[str, Node]) -> dict:
|
||||
""" create amplifiers json elements for the west direction.
|
||||
this includes the case where the case of a fused element defined instead of an
|
||||
ILA in eqpt sheet
|
||||
@@ -390,7 +430,13 @@ def create_west_eqpt_element(node):
|
||||
eqpt['params'] = {'loss': 0}
|
||||
return eqpt
|
||||
|
||||
def xls_to_json_data(input_filename, filter_region=[]):
|
||||
|
||||
def xls_to_json_data(input_filename: Path, filter_region: List[str] = None) -> Dict:
|
||||
"""Read the excel sheets and produces the json dict in GNPy format (legacy)
|
||||
returns json dict
|
||||
"""
|
||||
if filter_region is None:
|
||||
filter_region = []
|
||||
nodes, links, eqpts, roadms = parse_excel(input_filename)
|
||||
if filter_region:
|
||||
nodes = [n for n in nodes if n.region.lower() in filter_region]
|
||||
@@ -400,16 +446,13 @@ def xls_to_json_data(input_filename, filter_region=[]):
|
||||
cities = {lnk.from_city for lnk in links} | {lnk.to_city for lnk in links}
|
||||
nodes = [n for n in nodes if n.city in cities]
|
||||
|
||||
global nodes_by_city
|
||||
nodes_by_city = {n.city: n for n in nodes}
|
||||
|
||||
global links_by_city
|
||||
links_by_city = defaultdict(list)
|
||||
for link in links:
|
||||
links_by_city[link.from_city].append(link)
|
||||
links_by_city[link.to_city].append(link)
|
||||
|
||||
global eqpts_by_city
|
||||
eqpts_by_city = defaultdict(list)
|
||||
for eqpt in eqpts:
|
||||
eqpts_by_city[eqpt.from_city].append(eqpt)
|
||||
@@ -475,7 +518,7 @@ def xls_to_json_data(input_filename, filter_region=[]):
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Edfa',
|
||||
'operational': {'gain_target': None,
|
||||
'tilt_target': 0}
|
||||
'tilt_target': None}
|
||||
} for x in nodes_by_city.values() if x.node_type.lower() == 'ila' and x.city not in eqpts_by_city] +
|
||||
[{'uid': f'east edfa in {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
@@ -484,25 +527,26 @@ def xls_to_json_data(input_filename, filter_region=[]):
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Edfa',
|
||||
'operational': {'gain_target': None,
|
||||
'tilt_target': 0}
|
||||
} for x in nodes_by_city.values() if x.node_type.lower() == 'ila' and x.city not in eqpts_by_city] +
|
||||
[create_east_eqpt_element(e) for e in eqpts] +
|
||||
[create_west_eqpt_element(e) for e in eqpts],
|
||||
'tilt_target': None}
|
||||
} for x in nodes_by_city.values() if x.node_type.lower() == 'ila' and x.city not in eqpts_by_city]
|
||||
+ [create_east_eqpt_element(e, nodes_by_city) for e in eqpts]
|
||||
+ [create_west_eqpt_element(e, nodes_by_city) for e in eqpts],
|
||||
'connections':
|
||||
list(chain.from_iterable([eqpt_connection_by_city(n.city)
|
||||
list(chain.from_iterable([eqpt_connection_by_city(n.city, eqpts_by_city, links_by_city, nodes_by_city)
|
||||
for n in nodes]))
|
||||
+
|
||||
list(chain.from_iterable(zip(
|
||||
[{'from_node': f'trx {x.city}',
|
||||
'to_node': f'roadm {x.city}'}
|
||||
+ list(chain.from_iterable(zip(
|
||||
[{'from_node': f'trx {x.city}', 'to_node': f'roadm {x.city}'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'],
|
||||
[{'from_node': f'roadm {x.city}',
|
||||
'to_node': f'trx {x.city}'}
|
||||
[{'from_node': f'roadm {x.city}', 'to_node': f'trx {x.city}'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'])))
|
||||
}
|
||||
|
||||
|
||||
def convert_file(input_filename, filter_region=[], output_json_file_name=None):
|
||||
def convert_file(input_filename: Path, filter_region: List[str] = None, output_json_file_name: Path = None):
|
||||
"""Save the conversion into
|
||||
"""
|
||||
if filter_region is None:
|
||||
filter_region = []
|
||||
data = xls_to_json_data(input_filename, filter_region)
|
||||
if output_json_file_name is None:
|
||||
output_json_file_name = input_filename.with_suffix('.json')
|
||||
@@ -512,79 +556,77 @@ def convert_file(input_filename, filter_region=[], output_json_file_name=None):
|
||||
return output_json_file_name
|
||||
|
||||
|
||||
def corresp_names(input_filename, network):
|
||||
def corresp_names(input_filename: Path, network: DiGraph):
|
||||
""" a function that builds the correspondance between names given in the excel,
|
||||
and names used in the json, and created by the autodesign.
|
||||
All names are listed
|
||||
"""
|
||||
nodes, links, eqpts, roadms = parse_excel(input_filename)
|
||||
nodes, links, eqpts, _ = parse_excel(input_filename)
|
||||
fused = [n.uid for n in network.nodes() if isinstance(n, Fused)]
|
||||
ila = [n.uid for n in network.nodes() if isinstance(n, Edfa)]
|
||||
|
||||
corresp_roadm = {x.city: [f'roadm {x.city}'] for x in nodes
|
||||
if x.node_type.lower() == 'roadm'}
|
||||
corresp_fused = {x.city: [f'west fused spans in {x.city}', f'east fused spans in {x.city}']
|
||||
for x in nodes if x.node_type.lower() == 'fused' and
|
||||
f'west fused spans in {x.city}' in fused and
|
||||
f'east fused spans in {x.city}' in fused}
|
||||
|
||||
for x in nodes if x.node_type.lower() == 'fused'
|
||||
and f'west fused spans in {x.city}' in fused
|
||||
and f'east fused spans in {x.city}' in fused}
|
||||
corresp_ila = defaultdict(list)
|
||||
# add the special cases when an ila is changed into a fused
|
||||
for my_e in eqpts:
|
||||
name = f'east edfa in {my_e.from_city} to {my_e.to_city}'
|
||||
if my_e.east_amp_type.lower() == 'fused' and name in fused:
|
||||
if my_e.from_city in corresp_fused.keys():
|
||||
corresp_fused[my_e.from_city].append(name)
|
||||
else:
|
||||
corresp_fused[my_e.from_city] = [name]
|
||||
corresp_fused.get(my_e.from_city, []).append(name)
|
||||
name = f'west edfa in {my_e.from_city} to {my_e.to_city}'
|
||||
if my_e.west_amp_type.lower() == 'fused' and name in fused:
|
||||
if my_e.from_city in corresp_fused.keys():
|
||||
corresp_fused[my_e.from_city].append(name)
|
||||
else:
|
||||
corresp_fused[my_e.from_city] = [name]
|
||||
corresp_fused.get(my_e.from_city, []).append(name)
|
||||
# build corresp ila based on eqpt sheet
|
||||
# start with east direction
|
||||
corresp_ila = {e.from_city: [f'east edfa in {e.from_city} to {e.to_city}']
|
||||
for e in eqpts if f'east edfa in {e.from_city} to {e.to_city}' in ila}
|
||||
# west direction, append name or create a new item in dict
|
||||
for my_e in eqpts:
|
||||
name = f'west edfa in {my_e.from_city} to {my_e.to_city}'
|
||||
if name in ila:
|
||||
if my_e.from_city in corresp_ila.keys():
|
||||
for name in [f'east edfa in {my_e.from_city} to {my_e.to_city}',
|
||||
f'west edfa in {my_e.from_city} to {my_e.to_city}']:
|
||||
if name in ila:
|
||||
corresp_ila[my_e.from_city].append(name)
|
||||
else:
|
||||
corresp_ila[my_e.from_city] = [name]
|
||||
# complete with potential autodesign names: amplifiers
|
||||
for my_l in links:
|
||||
name = f'Edfa0_fiber ({my_l.to_city} \u2192 {my_l.from_city})-{my_l.west_cable}'
|
||||
if name in ila:
|
||||
if my_l.from_city in corresp_ila.keys():
|
||||
# create names whatever the type and filter them out
|
||||
# from-to direction
|
||||
names = [f'Edfa_preamp_roadm {my_l.from_city}_from_fiber ({my_l.to_city} \u2192 {my_l.from_city})-{my_l.west_cable}',
|
||||
f'Edfa_booster_roadm {my_l.from_city}_to_fiber ({my_l.from_city} \u2192 {my_l.to_city})-{my_l.east_cable}']
|
||||
for name in names:
|
||||
if name in ila:
|
||||
# "east edfa in Stbrieuc to Rennes_STA" is equivalent name as
|
||||
# "Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056"
|
||||
# "Edfa_booster_roadm Stbrieuc_to_fiber (Lannion_CAS → Stbrieuc)-F056"
|
||||
# "west edfa in Stbrieuc to Rennes_STA" is equivalent name as
|
||||
# "Edfa0_fiber (Rennes_STA → Stbrieuc)-F057"
|
||||
# does not filter names: all types (except boosters) are created.
|
||||
# in case fibers are splitted the name here is a prefix
|
||||
# "Edfa_preamp_roadm Stbrieuc_to_fiber (Rennes_STA → Stbrieuc)-F057"
|
||||
# in case fibers are splitted the name here is a
|
||||
corresp_ila[my_l.from_city].append(name)
|
||||
else:
|
||||
corresp_ila[my_l.from_city] = [name]
|
||||
name = f'Edfa0_fiber ({my_l.from_city} \u2192 {my_l.to_city})-{my_l.east_cable}'
|
||||
if name in ila:
|
||||
if my_l.to_city in corresp_ila.keys():
|
||||
# to-from direction
|
||||
names = [f'Edfa_preamp_roadm {my_l.to_city}_from_fiber ({my_l.from_city} \u2192 {my_l.to_city})-{my_l.east_cable}',
|
||||
f'Edfa_booster_roadm {my_l.to_city}_to_fiber ({my_l.to_city} \u2192 {my_l.from_city})-{my_l.west_cable}']
|
||||
for name in names:
|
||||
if name in ila:
|
||||
corresp_ila[my_l.to_city].append(name)
|
||||
else:
|
||||
corresp_ila[my_l.to_city] = [name]
|
||||
for node in nodes:
|
||||
names = [f'east edfa in {node.city}', f'west edfa in {node.city}']
|
||||
for name in names:
|
||||
if name in ila:
|
||||
# "east edfa in Stbrieuc to Rennes_STA" (created with Eqpt) is equivalent name as
|
||||
# "east edfa in Stbrieuc" or "west edfa in Stbrieuc" (created with Links sheet)
|
||||
# depending on link node order
|
||||
corresp_ila[node.city].append(name)
|
||||
|
||||
# merge fused with ila:
|
||||
for key, val in corresp_fused.items():
|
||||
if key in corresp_ila.keys():
|
||||
corresp_ila[key].extend(val)
|
||||
else:
|
||||
corresp_ila[key] = val
|
||||
corresp_ila[key].extend(val)
|
||||
# no need of roadm booster
|
||||
return corresp_roadm, corresp_fused, corresp_ila
|
||||
|
||||
|
||||
def parse_excel(input_filename):
|
||||
def parse_excel(input_filename: Path) -> Tuple[List[Node], List[Link], List[Eqpt], List[Roadm]]:
|
||||
"""reads xls(x) sheets among Nodes, Eqpts, Links, Roadms and parse the data in the sheets
|
||||
into internal data structure Node, Link, Eqpt, Roadm, classes
|
||||
"""
|
||||
link_headers = {
|
||||
'Node A': 'from_city',
|
||||
'Node Z': 'to_city',
|
||||
@@ -623,7 +665,6 @@ def parse_excel(input_filename):
|
||||
'Node Z': 'to_city',
|
||||
'east': {
|
||||
'amp type': 'east_amp_type',
|
||||
'att_in': 'east_att_in',
|
||||
'amp gain': 'east_amp_gain',
|
||||
'delta p': 'east_amp_dp',
|
||||
'tilt': 'east_tilt',
|
||||
@@ -631,7 +672,6 @@ def parse_excel(input_filename):
|
||||
},
|
||||
'west': {
|
||||
'amp type': 'west_amp_type',
|
||||
'att_in': 'west_att_in',
|
||||
'amp gain': 'west_amp_gain',
|
||||
'delta p': 'west_amp_dp',
|
||||
'tilt': 'west_tilt',
|
||||
@@ -640,7 +680,10 @@ def parse_excel(input_filename):
|
||||
}
|
||||
roadm_headers = {'Node A': 'from_node',
|
||||
'Node Z': 'to_node',
|
||||
'per degree target power (dBm)': 'target_pch_out_db'
|
||||
'per degree target power (dBm)': 'target_pch_out_db',
|
||||
'type_variety': 'type_variety',
|
||||
'from degrees': 'from_degrees',
|
||||
'from degree to degree impairment id': 'impairment_ids'
|
||||
}
|
||||
|
||||
with open_workbook(input_filename) as wb:
|
||||
@@ -648,36 +691,32 @@ def parse_excel(input_filename):
|
||||
links_sheet = wb.sheet_by_name('Links')
|
||||
try:
|
||||
eqpt_sheet = wb.sheet_by_name('Eqpt')
|
||||
except Exception:
|
||||
except XLRDError:
|
||||
# eqpt_sheet is optional
|
||||
eqpt_sheet = None
|
||||
try:
|
||||
roadm_sheet = wb.sheet_by_name('Roadms')
|
||||
except Exception:
|
||||
except XLRDError:
|
||||
# roadm_sheet is optional
|
||||
roadm_sheet = None
|
||||
|
||||
nodes = []
|
||||
for node in parse_sheet(nodes_sheet, node_headers, NODES_LINE, NODES_LINE + 1, NODES_COLUMN):
|
||||
nodes.append(Node(**node))
|
||||
nodes = [Node(**node) for node in parse_sheet(nodes_sheet, node_headers,
|
||||
NODES_LINE, NODES_LINE + 1, NODES_COLUMN)]
|
||||
expected_node_types = {'ROADM', 'ILA', 'FUSED'}
|
||||
for n in nodes:
|
||||
if n.node_type not in expected_node_types:
|
||||
n.node_type = 'ILA'
|
||||
|
||||
links = []
|
||||
for link in parse_sheet(links_sheet, link_headers, LINKS_LINE, LINKS_LINE + 2, LINKS_COLUMN):
|
||||
links.append(Link(**link))
|
||||
|
||||
links = [Link(**link) for link in parse_sheet(links_sheet, link_headers,
|
||||
LINKS_LINE, LINKS_LINE + 2, LINKS_COLUMN)]
|
||||
eqpts = []
|
||||
if eqpt_sheet is not None:
|
||||
for eqpt in parse_sheet(eqpt_sheet, eqpt_headers, EQPTS_LINE, EQPTS_LINE + 2, EQPTS_COLUMN):
|
||||
eqpts.append(Eqpt(**eqpt))
|
||||
|
||||
eqpts = [Eqpt(**eqpt) for eqpt in parse_sheet(eqpt_sheet, eqpt_headers,
|
||||
EQPTS_LINE, EQPTS_LINE + 2, EQPTS_COLUMN)]
|
||||
roadms = []
|
||||
if roadm_sheet is not None:
|
||||
for roadm in parse_sheet(roadm_sheet, roadm_headers, ROADMS_LINE, ROADMS_LINE+2, ROADMS_COLUMN):
|
||||
roadms.append(Roadm(**roadm))
|
||||
roadms = [Roadm(**roadm) for roadm in parse_sheet(roadm_sheet, roadm_headers,
|
||||
ROADMS_LINE, ROADMS_LINE + 2, ROADMS_COLUMN)]
|
||||
|
||||
# sanity check
|
||||
all_cities = Counter(n.city for n in nodes)
|
||||
@@ -699,32 +738,37 @@ def parse_excel(input_filename):
|
||||
return nodes, links, eqpts, roadms
|
||||
|
||||
|
||||
def eqpt_connection_by_city(city_name):
|
||||
other_cities = fiber_dest_from_source(city_name)
|
||||
def eqpt_connection_by_city(city_name: str, eqpts_by_city: DefaultDict[str, List[Eqpt]],
|
||||
links_by_city: DefaultDict[str, List[Link]], nodes_by_city: Dict[str, Node]) -> list:
|
||||
"""
|
||||
"""
|
||||
other_cities = fiber_dest_from_source(city_name, links_by_city)
|
||||
subdata = []
|
||||
if nodes_by_city[city_name].node_type.lower() in {'ila', 'fused'}:
|
||||
# Then len(other_cities) == 2
|
||||
direction = ['west', 'east']
|
||||
for i in range(2):
|
||||
from_ = fiber_link(other_cities[i], city_name)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_cities[0], direction[i])
|
||||
to_ = fiber_link(city_name, other_cities[1 - i])
|
||||
from_ = fiber_link(other_cities[i], city_name, links_by_city)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_cities[0], eqpts_by_city, nodes_by_city, direction[i])
|
||||
to_ = fiber_link(city_name, other_cities[1 - i], links_by_city)
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
elif nodes_by_city[city_name].node_type.lower() == 'roadm':
|
||||
for other_city in other_cities:
|
||||
from_ = f'roadm {city_name}'
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city)
|
||||
to_ = fiber_link(city_name, other_city)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city, eqpts_by_city, nodes_by_city)
|
||||
to_ = fiber_link(city_name, other_city, links_by_city)
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
|
||||
from_ = fiber_link(other_city, city_name)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city, "west")
|
||||
from_ = fiber_link(other_city, city_name, links_by_city)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city, eqpts_by_city, nodes_by_city, "west")
|
||||
to_ = f'roadm {city_name}'
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
return subdata
|
||||
|
||||
|
||||
def connect_eqpt(from_, in_, to_):
|
||||
def connect_eqpt(from_: str, in_: str, to_: str) -> List[dict]:
|
||||
"""Utils: create the topology connection json dict between in and to
|
||||
"""
|
||||
connections = []
|
||||
if in_ != '':
|
||||
connections = [{'from_node': from_, 'to_node': in_},
|
||||
@@ -734,7 +778,11 @@ def connect_eqpt(from_, in_, to_):
|
||||
return connections
|
||||
|
||||
|
||||
def eqpt_in_city_to_city(in_city, to_city, direction='east'):
|
||||
def eqpt_in_city_to_city(in_city: str, to_city: str,
|
||||
eqpts_by_city: DefaultDict[str, List[Eqpt]], nodes_by_city: Dict[str, Node],
|
||||
direction: str = 'east') -> str:
|
||||
"""Utils: returns the formatted dtring corresponding to in_city types and direction
|
||||
"""
|
||||
rev_direction = 'west' if direction == 'east' else 'east'
|
||||
return_eqpt = ''
|
||||
if in_city in eqpts_by_city:
|
||||
@@ -753,22 +801,25 @@ def eqpt_in_city_to_city(in_city, to_city, direction='east'):
|
||||
return return_eqpt
|
||||
|
||||
|
||||
def corresp_next_node(network, corresp_ila, corresp_roadm):
|
||||
def corresp_next_node(network: DiGraph, corresp_ila: dict, corresp_roadm: dict) -> Tuple[dict, dict]:
|
||||
""" for each name in corresp dictionnaries find the next node in network and its name
|
||||
given by user in excel. for meshTopology_exampleV2.xls:
|
||||
user ILA name Stbrieuc covers the two direction. convert.py creates 2 different ILA
|
||||
with possible names (depending on the direction and if the eqpt was defined in eqpt
|
||||
sheet)
|
||||
for an ILA and if it is defined in eqpt:
|
||||
- east edfa in Stbrieuc to Rennes_STA
|
||||
- west edfa in Stbrieuc to Rennes_STA
|
||||
- Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056
|
||||
- Edfa0_fiber (Rennes_STA → Stbrieuc)-F057
|
||||
for an ILA and if it is not defined in eqpt:
|
||||
- east edfa in Stbrieuc
|
||||
- west edfa in Stbrieuc
|
||||
for a roadm
|
||||
"Edfa_preamp_roadm node1_from_fiber (siteE → node1)-CABLES#19"
|
||||
"Edfa_booster_roadm node1_to_fiber (node1 → siteE)-CABLES#19"
|
||||
next_nodes finds the user defined name of next node to be able to map the path constraints
|
||||
- east edfa in Stbrieuc to Rennes_STA next node = Rennes_STA
|
||||
- west edfa in Stbrieuc to Rennes_STA next node Lannion_CAS
|
||||
|
||||
Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056 and Edfa0_fiber (Rennes_STA → Stbrieuc)-F057
|
||||
do not exist
|
||||
the function supports fiber splitting, fused nodes and shall only be called if
|
||||
excel format is used for both network and service
|
||||
"""
|
||||
@@ -779,8 +830,8 @@ def corresp_next_node(network, corresp_ila, corresp_roadm):
|
||||
for ila_elem in ila_list:
|
||||
# find the node with ila_elem string _in_ the node uid. 'in' is used instead of
|
||||
# '==' to find composed nodes due to fiber splitting in autodesign.
|
||||
# eg if elem_ila is 'Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056',
|
||||
# node uid 'Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056_(1/2)' is possible
|
||||
# eg if elem_ila is 'east edfa in Stbrieuc to Rennes_STA',
|
||||
# node uid 'east edfa in Stbrieuc to Rennes_STA-_(1/2)' is possible
|
||||
correct_ila_name = next(n.uid for n in network.nodes() if ila_elem in n.uid)
|
||||
temp.remove(ila_elem)
|
||||
temp.append(correct_ila_name)
|
||||
@@ -797,7 +848,7 @@ def corresp_next_node(network, corresp_ila, corresp_roadm):
|
||||
break
|
||||
# if next_nd was not already added in the dict with the previous loop,
|
||||
# add the first found correspondance in ila names
|
||||
if correct_ila_name not in next_node.keys():
|
||||
if correct_ila_name not in next_node:
|
||||
for key, val in corresp_ila.items():
|
||||
# in case of splitted fibers the ila name might not be exact match
|
||||
if [e for e in val if e in next_nd.uid]:
|
||||
@@ -808,7 +859,9 @@ def corresp_next_node(network, corresp_ila, corresp_roadm):
|
||||
return corresp_ila, next_node
|
||||
|
||||
|
||||
def fiber_dest_from_source(city_name):
|
||||
def fiber_dest_from_source(city_name: str, links_by_city: DefaultDict[str, List[Link]]) -> List[str]:
|
||||
"""Returns the list of cities city_name is connected to
|
||||
"""
|
||||
destinations = []
|
||||
links_from_city = links_by_city[city_name]
|
||||
for l in links_from_city:
|
||||
@@ -819,7 +872,9 @@ def fiber_dest_from_source(city_name):
|
||||
return destinations
|
||||
|
||||
|
||||
def fiber_link(from_city, to_city):
|
||||
def fiber_link(from_city: str, to_city: str, links_by_city: DefaultDict[str, List[Link]]) -> str:
|
||||
"""utils: returns formatted uid for fibers between from_city and to_city
|
||||
"""
|
||||
source_dest = (from_city, to_city)
|
||||
links = links_by_city[from_city]
|
||||
link = next(l for l in links if l.from_city in source_dest and l.to_city in source_dest)
|
||||
@@ -830,7 +885,9 @@ def fiber_link(from_city, to_city):
|
||||
return fiber
|
||||
|
||||
|
||||
def midpoint(city_a, city_b):
|
||||
def midpoint(city_a: Node, city_b:Node) -> dict:
|
||||
"""Computes mipoint coordinates
|
||||
"""
|
||||
lats = city_a.latitude, city_b.latitude
|
||||
longs = city_a.longitude, city_b.longitude
|
||||
try:
|
||||
@@ -855,10 +912,12 @@ LINKS_LINE = 3
|
||||
EQPTS_LINE = 3
|
||||
EQPTS_COLUMN = 14
|
||||
ROADMS_LINE = 3
|
||||
ROADMS_COLUMN = 3
|
||||
ROADMS_COLUMN = 6
|
||||
|
||||
|
||||
def _do_convert():
|
||||
"""Main function for xls(x) topology conversion to JSON format
|
||||
"""
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('workbook', type=Path)
|
||||
parser.add_argument('-f', '--filter-region', action='append', default=[])
|
||||
|
||||
@@ -8,13 +8,15 @@ gnpy.tools.json_io
|
||||
Loading and saving data from JSON files in GNPy's internal data format
|
||||
"""
|
||||
|
||||
from networkx import DiGraph
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from numpy import arange
|
||||
from copy import deepcopy
|
||||
from typing import Union, Dict, List
|
||||
from networkx import DiGraph
|
||||
from numpy import arange
|
||||
|
||||
|
||||
from gnpy.core import elements
|
||||
from gnpy.core.equipment import trx_mode_params, find_type_variety
|
||||
@@ -40,15 +42,21 @@ Model_dual_stage = namedtuple('Model_dual_stage', 'preamp_variety booster_variet
|
||||
|
||||
|
||||
class Model_openroadm_preamp:
|
||||
pass
|
||||
"""class to hold nf model specific to OpenROADM preamp
|
||||
"""
|
||||
|
||||
|
||||
class Model_openroadm_booster:
|
||||
pass
|
||||
"""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))
|
||||
@@ -60,6 +68,8 @@ class _JsonThing:
|
||||
|
||||
|
||||
class SI(_JsonThing):
|
||||
"""Spectrum Information
|
||||
"""
|
||||
default_values = {
|
||||
"f_min": 191.35e12,
|
||||
"f_max": 196.1e12,
|
||||
@@ -78,6 +88,8 @@ class SI(_JsonThing):
|
||||
|
||||
|
||||
class Span(_JsonThing):
|
||||
"""Span simulations definition
|
||||
"""
|
||||
default_values = {
|
||||
'power_mode': True,
|
||||
'delta_power_range_db': None,
|
||||
@@ -97,6 +109,8 @@ class Span(_JsonThing):
|
||||
|
||||
|
||||
class Roadm(_JsonThing):
|
||||
"""List of ROADM and their specs
|
||||
"""
|
||||
default_values = {
|
||||
'type_variety': 'default',
|
||||
'add_drop_osnr': 100,
|
||||
@@ -129,6 +143,8 @@ class Roadm(_JsonThing):
|
||||
|
||||
|
||||
class Transceiver(_JsonThing):
|
||||
"""List of transceivers and their modes
|
||||
"""
|
||||
default_values = {
|
||||
'type_variety': None,
|
||||
'frequency': None,
|
||||
@@ -161,6 +177,8 @@ class Transceiver(_JsonThing):
|
||||
|
||||
|
||||
class Fiber(_JsonThing):
|
||||
"""Fiber default settings
|
||||
"""
|
||||
default_values = {
|
||||
'type_variety': '',
|
||||
'dispersion': None,
|
||||
@@ -181,10 +199,13 @@ class Fiber(_JsonThing):
|
||||
|
||||
|
||||
class RamanFiber(Fiber):
|
||||
pass
|
||||
"""Raman Fiber default settings
|
||||
"""
|
||||
|
||||
|
||||
class Amp(_JsonThing):
|
||||
"""List of amplifiers with their specs
|
||||
"""
|
||||
default_values = EdfaParams.default_values
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -192,6 +213,8 @@ class Amp(_JsonThing):
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, filename, **kwargs):
|
||||
"""
|
||||
"""
|
||||
config = Path(filename).parent / 'default_edfa_config.json'
|
||||
# default_edfa_config.json assumes a DGT profile independantly from fmin/fmax, that's a generic profile
|
||||
type_variety = kwargs['type_variety']
|
||||
@@ -203,9 +226,9 @@ class Amp(_JsonThing):
|
||||
if type_def == 'fixed_gain':
|
||||
try:
|
||||
nf0 = kwargs.pop('nf0')
|
||||
except KeyError: # nf0 is expected for a fixed gain amp
|
||||
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)
|
||||
raise EquipmentConfigError(msg) from exc
|
||||
for k in ('nf_min', 'nf_max'):
|
||||
try:
|
||||
del kwargs[k]
|
||||
@@ -219,9 +242,9 @@ class Amp(_JsonThing):
|
||||
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:
|
||||
except KeyError as exc:
|
||||
msg = f'missing nf_min or nf_max value input for amplifier: {type_variety} in equipment config'
|
||||
raise EquipmentConfigError(msg)
|
||||
raise EquipmentConfigError(msg) from exc
|
||||
try: # remove all remaining nf inputs
|
||||
del kwargs['nf0']
|
||||
except KeyError:
|
||||
@@ -231,8 +254,8 @@ class Amp(_JsonThing):
|
||||
elif type_def == 'openroadm':
|
||||
try:
|
||||
nf_coef = kwargs.pop('nf_coef')
|
||||
except KeyError: # nf_coef is expected for openroadm amp
|
||||
raise EquipmentConfigError(f'missing nf_coef input for amplifier: {type_variety} in equipment config')
|
||||
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()
|
||||
@@ -242,9 +265,9 @@ class Amp(_JsonThing):
|
||||
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:
|
||||
except KeyError as exc:
|
||||
raise EquipmentConfigError(f'missing preamp/booster variety input for amplifier: {type_variety}'
|
||||
+ ' in equipment config')
|
||||
+ ' in equipment config') from exc
|
||||
dual_stage_def = Model_dual_stage(preamp_variety, booster_variety)
|
||||
elif type_def == 'multi_band':
|
||||
amplifiers = kwargs['amplifiers']
|
||||
@@ -264,15 +287,7 @@ class Amp(_JsonThing):
|
||||
'nf_model': nf_def, 'dual_stage_model': dual_stage_def, 'multi_band': amplifiers})
|
||||
|
||||
|
||||
def _automatic_spacing(baud_rate):
|
||||
"""return the min possible channel spacing for a given baud rate"""
|
||||
# TODO : this should parametrized in a cfg file
|
||||
# list of possible tuples [(max_baud_rate, spacing_for_this_baud_rate)]
|
||||
spacing_list = [(33e9, 37.5e9), (38e9, 50e9), (50e9, 62.5e9), (67e9, 75e9), (92e9, 100e9)]
|
||||
return min((s[1] for s in spacing_list if s[0] > baud_rate), default=baud_rate * 1.2)
|
||||
|
||||
|
||||
def _spectrum_from_json(json_data):
|
||||
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.
|
||||
@@ -314,17 +329,13 @@ def _spectrum_from_json(json_data):
|
||||
previous_part_max_freq = 0.0
|
||||
for index, part in enumerate(json_data):
|
||||
# default delta_pdb is 0 dB
|
||||
if 'delta_pdb' not in part:
|
||||
part['delta_pdb'] = 0
|
||||
part.setdefault('delta_pdb', 0)
|
||||
# add a label to the partition for the printings
|
||||
if 'label' not in part:
|
||||
part['label'] = f'{index}-{part["baud_rate"] * 1e-9 :.2f}G'
|
||||
part.setdefault('label', f'{index}-{part["baud_rate"] * 1e-9:.2f}G')
|
||||
# default tx_osnr is set to 40 dB
|
||||
if 'tx_osnr' not in part:
|
||||
part['tx_osnr'] = 40
|
||||
part.setdefault('tx_osnr', 40)
|
||||
# default tx_power_dbm is set to 0 dBn
|
||||
if 'tx_power_dbm' not in part:
|
||||
part['tx_power_dbm'] = 0
|
||||
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
|
||||
@@ -332,12 +343,13 @@ def _spectrum_from_json(json_data):
|
||||
# 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 ' +\
|
||||
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'
|
||||
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']):
|
||||
@@ -349,17 +361,26 @@ def _spectrum_from_json(json_data):
|
||||
return spectrum
|
||||
|
||||
|
||||
def load_equipment(filename):
|
||||
def load_equipment(filename: Path) -> dict:
|
||||
"""Load equipment, returns equipment dict
|
||||
"""
|
||||
json_data = load_json(filename)
|
||||
return _equipment_from_json(json_data, filename)
|
||||
|
||||
|
||||
def load_initial_spectrum(filename):
|
||||
def load_initial_spectrum(filename: Path) -> dict:
|
||||
"""Load spectrum to propagate, returns spectrum dict
|
||||
"""
|
||||
json_data = load_json(filename)
|
||||
return _spectrum_from_json(json_data['spectrum'])
|
||||
|
||||
|
||||
def _update_dual_stage(equipment):
|
||||
def _update_dual_stage(equipment: dict) -> dict:
|
||||
"""Update attributes of all dual stage amps with the preamp and booster attributes
|
||||
(defined in the equipment dictionary)
|
||||
|
||||
Returns the updated equiment dictionary
|
||||
"""
|
||||
edfa_dict = equipment['Edfa']
|
||||
for edfa in edfa_dict.values():
|
||||
if edfa.type_def == 'dual_stage':
|
||||
@@ -403,7 +424,7 @@ def _update_band(equipment: dict) -> dict:
|
||||
return equipment
|
||||
|
||||
|
||||
def _roadm_restrictions_sanity_check(equipment):
|
||||
def _roadm_restrictions_sanity_check(equipment: dict):
|
||||
"""verifies that booster and preamp restrictions specified in roadm equipment are listed in the edfa."""
|
||||
for roadm_type, roadm_eqpt in equipment['Roadm'].items():
|
||||
restrictions = roadm_eqpt.restrictions['booster_variety_list'] + \
|
||||
@@ -414,7 +435,7 @@ def _roadm_restrictions_sanity_check(equipment):
|
||||
+ 'defined EDFA name')
|
||||
|
||||
|
||||
def _check_fiber_vs_raman_fiber(equipment):
|
||||
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
|
||||
@@ -429,7 +450,7 @@ def _check_fiber_vs_raman_fiber(equipment):
|
||||
f'disagrees for "{attr}": {a} != {b}')
|
||||
|
||||
|
||||
def _equipment_from_json(json_data, filename):
|
||||
def _equipment_from_json(json_data: dict, filename: Path) -> 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)
|
||||
@@ -474,7 +495,12 @@ def _equipment_from_json(json_data, filename):
|
||||
return equipment
|
||||
|
||||
|
||||
def load_network(filename, 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':
|
||||
@@ -498,21 +524,22 @@ def _cls_for(equipment_type):
|
||||
return elements.Edfa
|
||||
if equipment_type == 'Fused':
|
||||
return elements.Fused
|
||||
elif equipment_type == 'Roadm':
|
||||
if equipment_type == 'Roadm':
|
||||
return elements.Roadm
|
||||
elif equipment_type == 'Transceiver':
|
||||
if equipment_type == 'Transceiver':
|
||||
return elements.Transceiver
|
||||
elif equipment_type == 'Fiber':
|
||||
if equipment_type == 'Fiber':
|
||||
return elements.Fiber
|
||||
elif equipment_type == 'RamanFiber':
|
||||
if equipment_type == 'RamanFiber':
|
||||
return elements.RamanFiber
|
||||
elif equipment_type == 'Multiband_amplifier':
|
||||
if equipment_type == 'Multiband_amplifier':
|
||||
return elements.Multiband_amplifier
|
||||
else:
|
||||
raise ConfigurationError(f'Unknown network equipment "{equipment_type}"')
|
||||
raise ConfigurationError(f'Unknown network equipment "{equipment_type}"')
|
||||
|
||||
|
||||
def network_from_json(json_data, equipment):
|
||||
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
|
||||
@@ -553,7 +580,7 @@ def network_from_json(json_data, equipment):
|
||||
except ConfigurationError as e:
|
||||
msg = f'Node {el_config["uid"]}: {e}'
|
||||
raise ConfigurationError(msg)
|
||||
if variety is not None and variety != multiband_type_variety:
|
||||
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:
|
||||
@@ -608,14 +635,16 @@ def network_from_json(json_data, equipment):
|
||||
else:
|
||||
edge_length = 0.01
|
||||
g.add_edge(nodes[from_node], nodes[to_node], weight=edge_length)
|
||||
except KeyError:
|
||||
except KeyError as exc:
|
||||
msg = f'can not find {from_node} or {to_node} defined in {cx}'
|
||||
raise NetworkTopologyError(msg)
|
||||
raise NetworkTopologyError(msg) from exc
|
||||
|
||||
return g
|
||||
|
||||
|
||||
def network_to_json(network):
|
||||
def network_to_json(network: DiGraph) -> dict:
|
||||
"""Export network graph as a json dict
|
||||
"""
|
||||
data = {
|
||||
'elements': [n.to_json for n in network]
|
||||
}
|
||||
@@ -629,53 +658,61 @@ def network_to_json(network):
|
||||
return data
|
||||
|
||||
|
||||
def load_json(filename):
|
||||
def load_json(filename: Path) -> dict:
|
||||
"""load json data, convert from the yang to the legacy
|
||||
supports both legacy ang yang formatted inputs based on yang models
|
||||
"""
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def save_json(obj, filename):
|
||||
def save_json(obj: Dict, filename: Path):
|
||||
"""Save in json format. Use yang formatted data for Topo and Services
|
||||
"""
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(obj, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def load_requests(filename, eqpt, bidir, network, network_filename):
|
||||
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}')
|
||||
raise ServiceError(f'Service error: {this_e}') from this_e
|
||||
else:
|
||||
return load_json(filename)
|
||||
|
||||
|
||||
def requests_from_json(json_data, equipment):
|
||||
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
|
||||
params = {}
|
||||
params['request_id'] = f'{req["request-id"]}'
|
||||
params['source'] = req['source']
|
||||
params['bidir'] = req['bidirectional']
|
||||
params['destination'] = req['destination']
|
||||
params['trx_type'] = req['path-constraints']['te-bandwidth']['trx_type']
|
||||
if params['trx_type'] is None:
|
||||
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)
|
||||
params['trx_mode'] = req['path-constraints']['te-bandwidth'].get('trx_mode', None)
|
||||
params['format'] = params['trx_mode']
|
||||
params['spacing'] = req['path-constraints']['te-bandwidth']['spacing']
|
||||
try:
|
||||
nd_list = sorted(req['explicit-route-objects']['route-object-include-exclude'], key=lambda x: x['index'])
|
||||
except KeyError:
|
||||
nd_list = []
|
||||
params['nodes_list'] = [n['num-unnum-hop']['node-id'] for n in nd_list]
|
||||
params['loose_list'] = [n['num-unnum-hop']['hop-type'] for n in 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:
|
||||
@@ -721,7 +758,7 @@ def requests_from_json(json_data, equipment):
|
||||
return requests_list
|
||||
|
||||
|
||||
def _check_one_request(params, f_max_from_si):
|
||||
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']
|
||||
@@ -731,19 +768,19 @@ def _check_one_request(params, f_max_from_si):
|
||||
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'
|
||||
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' 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, requested_m = compute_spectrum_slot_vs_bandwidth(params['path_bandwidth'],
|
||||
params['spacing'],
|
||||
params['bit_rate'])
|
||||
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'])
|
||||
@@ -782,7 +819,7 @@ def _check_one_request(params, f_max_from_si):
|
||||
i += 1
|
||||
|
||||
|
||||
def disjunctions_from_json(json_data):
|
||||
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
|
||||
"""
|
||||
@@ -801,20 +838,30 @@ def disjunctions_from_json(json_data):
|
||||
|
||||
|
||||
def convert_service_sheet(
|
||||
input_filename,
|
||||
eqpt,
|
||||
network,
|
||||
network_filename=None,
|
||||
output_filename='',
|
||||
bidir=False):
|
||||
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'
|
||||
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, equalization_types):
|
||||
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,
|
||||
@@ -831,7 +878,7 @@ def find_equalisation(params, equalization_types):
|
||||
return equalization
|
||||
|
||||
|
||||
def merge_equalization(params, extra_params):
|
||||
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)
|
||||
|
||||
@@ -11,105 +11,150 @@ Yang model for requesting path computation.
|
||||
See: draft-ietf-teas-yang-path-computation-01.txt
|
||||
"""
|
||||
|
||||
from xlrd import open_workbook, XL_CELL_EMPTY
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from networkx import DiGraph
|
||||
from xlrd import open_workbook, XL_CELL_EMPTY
|
||||
|
||||
from gnpy.core.utils import db2lin
|
||||
from gnpy.core.exceptions import ServiceError
|
||||
from gnpy.core.elements import Transceiver, Roadm, Edfa, Fiber
|
||||
from gnpy.tools.convert import corresp_names, corresp_next_node
|
||||
from gnpy.tools.convert import corresp_names, corresp_next_node, all_rows
|
||||
|
||||
SERVICES_COLUMN = 12
|
||||
|
||||
|
||||
def all_rows(sheet, start=0):
|
||||
return (sheet.row(x) for x in range(start, sheet.nrows))
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Request(namedtuple('Request', 'request_id source destination trx_type mode \
|
||||
spacing power nb_channel disjoint_from nodes_list is_loose path_bandwidth')):
|
||||
def __new__(cls, request_id, source, destination, trx_type, mode=None, spacing=None, power=None, nb_channel=None, disjoint_from='', nodes_list=None, is_loose='', path_bandwidth=None):
|
||||
return super().__new__(cls, request_id, source, destination, trx_type, mode, spacing, power, nb_channel, disjoint_from, nodes_list, is_loose, path_bandwidth)
|
||||
class Request(namedtuple('request_param', 'request_id source destination trx_type mode \
|
||||
spacing power nb_channel disjoint_from nodes_list is_loose path_bandwidth')):
|
||||
"""DATA class for a request.
|
||||
|
||||
:params request_id (int): The unique identifier for the request.
|
||||
:params source (str): The source node for the communication.
|
||||
:params destination (str): The destination node for the communication.
|
||||
:params trx_type (str): The type of transmission for the communication.
|
||||
:params mode (str, optional): The mode of transmission. Defaults to None.
|
||||
:params spacing (float, optional): The spacing between channels. Defaults to None.
|
||||
:params power (float, optional): The power level for the communication. Defaults to None.
|
||||
:params nb_channel (int, optional): The number of channels required for the communication. Defaults to None.
|
||||
:params disjoint_from (str, optional): The node to be disjoint from. Defaults to ''.
|
||||
:params nodes_list (list, optional): The list of nodes involved in the communication. Defaults to None.
|
||||
:params is_loose (str, optional): Indicates if the communication is loose. Defaults to ''.
|
||||
:params path_bandwidth (float, optional): The bandwidth required for the communication. Defaults to None.
|
||||
"""
|
||||
def __new__(cls, request_id, source, destination, trx_type, mode=None, spacing=None, power=None, nb_channel=None,
|
||||
disjoint_from='', nodes_list=None, is_loose='', path_bandwidth=None):
|
||||
return super().__new__(cls, request_id, source, destination, trx_type, mode, spacing, power, nb_channel,
|
||||
disjoint_from, nodes_list, is_loose, path_bandwidth)
|
||||
|
||||
|
||||
class Element:
|
||||
"""
|
||||
"""
|
||||
def __init__(self, uid):
|
||||
self.uid = uid
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.uid == other.uid
|
||||
return isinstance(other, type(self)) and self.uid == other.ui
|
||||
|
||||
def __hash__(self):
|
||||
return hash((type(self), self.uid))
|
||||
|
||||
|
||||
class Request_element(Element):
|
||||
def __init__(self, Request, equipment, bidir):
|
||||
"""Class that generate the request in the json format
|
||||
|
||||
:params request_param (Request): The request object containing the information for the element.
|
||||
:params equipment (dict): The equipment configuration for the communication.
|
||||
:params bidir (bool): Indicates if the communication is bidirectional.
|
||||
|
||||
Attributes:
|
||||
request_id (str): The unique identifier for the request.
|
||||
source (str): The source node for the communication.
|
||||
destination (str): The destination node for the communication.
|
||||
srctpid (str): The source TP ID for the communication.
|
||||
dsttpid (str): The destination TP ID for the communication.
|
||||
bidir (bool): Indicates if the communication is bidirectional.
|
||||
trx_type (str): The type of transmission for the communication.
|
||||
mode (str): The mode of transmission for the communication.
|
||||
spacing (float): The spacing between channels for the communication.
|
||||
power (float): The power level for the communication.
|
||||
nb_channel (int): The number of channels required for the communication.
|
||||
disjoint_from (list): The list of nodes to be disjoint from.
|
||||
nodes_list (list): The list of nodes involved in the communication.
|
||||
loose (str): Indicates if the communication is loose or strict.
|
||||
path_bandwidth (float): The bandwidth required for the communication.
|
||||
"""
|
||||
def __init__(self, request_param: Request, equipment: Dict, bidir: bool):
|
||||
"""
|
||||
"""
|
||||
super().__init__(uid=request_param.request_id)
|
||||
# request_id is str
|
||||
# excel has automatic number formatting that adds .0 on integer values
|
||||
# the next lines recover the pure int value, assuming this .0 is unwanted
|
||||
self.request_id = correct_xlrd_int_to_str_reading(Request.request_id)
|
||||
self.source = f'trx {Request.source}'
|
||||
self.destination = f'trx {Request.destination}'
|
||||
self.request_id = correct_xlrd_int_to_str_reading(request_param.request_id)
|
||||
self.source = f'trx {request_param.source}'
|
||||
self.destination = f'trx {request_param.destination}'
|
||||
# TODO: the automatic naming generated by excel parser requires that source and dest name
|
||||
# be a string starting with 'trx' : this is manually added here.
|
||||
self.srctpid = f'trx {Request.source}'
|
||||
self.dsttpid = f'trx {Request.destination}'
|
||||
self.srctpid = f'trx {request_param.source}'
|
||||
self.dsttpid = f'trx {request_param.destination}'
|
||||
self.bidir = bidir
|
||||
# test that trx_type belongs to eqpt_config.json
|
||||
# if not replace it with a default
|
||||
try:
|
||||
if equipment['Transceiver'][Request.trx_type]:
|
||||
self.trx_type = correct_xlrd_int_to_str_reading(Request.trx_type)
|
||||
if Request.mode is not None:
|
||||
Requestmode = correct_xlrd_int_to_str_reading(Request.mode)
|
||||
if [mode for mode in equipment['Transceiver'][Request.trx_type].mode if mode['format'] == Requestmode]:
|
||||
self.mode = Requestmode
|
||||
if equipment['Transceiver'][request_param.trx_type]:
|
||||
self.trx_type = correct_xlrd_int_to_str_reading(request_param.trx_type)
|
||||
if request_param.mode is not None:
|
||||
request_mode = correct_xlrd_int_to_str_reading(request_param.mode)
|
||||
if [mode for mode in equipment['Transceiver'][request_param.trx_type].mode
|
||||
if mode['format'] == request_mode]:
|
||||
self.mode = request_mode
|
||||
else:
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' ' \
|
||||
+ f'with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.'
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{request_param.trx_type}\' ' \
|
||||
+ f'with mode: \'{request_mode}\' in eqpt library \nComputation stopped.'
|
||||
raise ServiceError(msg)
|
||||
else:
|
||||
Requestmode = None
|
||||
self.mode = Request.mode
|
||||
except KeyError:
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' ' \
|
||||
+ f'with mode: \'{Request.mode}\' in eqpt library \nComputation stopped.'
|
||||
raise ServiceError(msg)
|
||||
request_mode = None
|
||||
self.mode = request_param.mode
|
||||
except KeyError as e:
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{request_param.trx_type}\' with mode: ' \
|
||||
+ f'\'{request_param.mode}\' in eqpt library \nComputation stopped.'
|
||||
raise ServiceError(msg) from e
|
||||
# excel input are in GHz and dBm
|
||||
if Request.spacing is not None:
|
||||
self.spacing = Request.spacing * 1e9
|
||||
if request_param.spacing is not None:
|
||||
self.spacing = request_param.spacing * 1e9
|
||||
else:
|
||||
msg = f'Request {self.request_id} missing spacing: spacing is mandatory.\ncomputation stopped'
|
||||
raise ServiceError(msg)
|
||||
if Request.power is not None:
|
||||
self.power = db2lin(Request.power) * 1e-3
|
||||
else:
|
||||
self.power = None
|
||||
if Request.nb_channel is not None:
|
||||
self.nb_channel = int(Request.nb_channel)
|
||||
else:
|
||||
self.nb_channel = None
|
||||
self.power = None
|
||||
if request_param.power is not None:
|
||||
self.power = db2lin(request_param.power) * 1e-3
|
||||
self.nb_channel = None
|
||||
if request_param.nb_channel is not None:
|
||||
self.nb_channel = int(request_param.nb_channel)
|
||||
|
||||
value = correct_xlrd_int_to_str_reading(Request.disjoint_from)
|
||||
value = correct_xlrd_int_to_str_reading(request_param.disjoint_from)
|
||||
self.disjoint_from = [n for n in value.split(' | ') if value]
|
||||
self.nodes_list = []
|
||||
if Request.nodes_list:
|
||||
self.nodes_list = Request.nodes_list.split(' | ')
|
||||
if request_param.nodes_list:
|
||||
self.nodes_list = request_param.nodes_list.split(' | ')
|
||||
self.loose = 'LOOSE'
|
||||
if Request.is_loose.lower() == 'no':
|
||||
if request_param.is_loose.lower() == 'no':
|
||||
self.loose = 'STRICT'
|
||||
self.path_bandwidth = None
|
||||
if Request.path_bandwidth is not None:
|
||||
self.path_bandwidth = Request.path_bandwidth * 1e9
|
||||
else:
|
||||
self.path_bandwidth = 0
|
||||
|
||||
uid = property(lambda self: repr(self))
|
||||
self.path_bandwidth = 0
|
||||
if request_param.path_bandwidth is not None:
|
||||
self.path_bandwidth = request_param.path_bandwidth * 1e9
|
||||
|
||||
@property
|
||||
def pathrequest(self):
|
||||
"""Creates json dictionnary for the request
|
||||
"""
|
||||
# Default assumption for bidir is False
|
||||
req_dictionnary = {
|
||||
'request-id': self.request_id,
|
||||
@@ -152,29 +197,32 @@ class Request_element(Element):
|
||||
|
||||
@property
|
||||
def pathsync(self):
|
||||
"""Creates json dictionnary for disjunction list (synchronization vector)
|
||||
"""
|
||||
if self.disjoint_from:
|
||||
return {'synchronization-id': self.request_id,
|
||||
'svec': {
|
||||
'relaxable': 'false',
|
||||
'disjointness': 'node link',
|
||||
'request-id-number': [self.request_id] + [n for n in self.disjoint_from]
|
||||
'request-id-number': [self.request_id] + list(self.disjoint_from)
|
||||
}
|
||||
}
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
# TO-DO: avoid multiple entries with same synchronisation vectors
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""Returns the json dictionnary for requests and for synchronisation vector
|
||||
"""
|
||||
return self.pathrequest, self.pathsync
|
||||
|
||||
|
||||
def read_service_sheet(
|
||||
input_filename,
|
||||
eqpt,
|
||||
network,
|
||||
network_filename=None,
|
||||
bidir=False):
|
||||
input_filename: Path,
|
||||
eqpt: Dict,
|
||||
network: DiGraph,
|
||||
network_filename: Path = None,
|
||||
bidir: bool = False) -> Dict:
|
||||
""" converts a service sheet into a json structure
|
||||
"""
|
||||
if network_filename is None:
|
||||
@@ -184,19 +232,16 @@ def read_service_sheet(
|
||||
req = correct_xls_route_list(network_filename, network, req)
|
||||
# if there is no sync vector , do not write any synchronization
|
||||
synchro = [n.json[1] for n in req if n.json[1] is not None]
|
||||
data = {'path-request': [n.json[0] for n in req]}
|
||||
if synchro:
|
||||
data = {
|
||||
'path-request': [n.json[0] for n in req],
|
||||
'synchronization': synchro
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
'path-request': [n.json[0] for n in req]
|
||||
}
|
||||
data['synchronization'] = synchro
|
||||
return data
|
||||
|
||||
|
||||
def correct_xlrd_int_to_str_reading(v):
|
||||
"""Utils: ensure that int values in id are read as strings containing the int and
|
||||
do not use the automatic float conversion from xlrd
|
||||
"""
|
||||
if not isinstance(v, str):
|
||||
value = str(int(v))
|
||||
if value.endswith('.0'):
|
||||
@@ -206,22 +251,27 @@ def correct_xlrd_int_to_str_reading(v):
|
||||
return value
|
||||
|
||||
|
||||
def parse_row(row, fieldnames):
|
||||
def parse_row(row: List, fieldnames: List[str]) -> Dict:
|
||||
"""Reads each values in a row and creates a dict using field names
|
||||
"""
|
||||
return {f: r.value for f, r in zip(fieldnames, row[0:SERVICES_COLUMN])
|
||||
if r.ctype != XL_CELL_EMPTY}
|
||||
|
||||
|
||||
def parse_excel(input_filename):
|
||||
def parse_excel(input_filename: Path) -> List[Request]:
|
||||
"""Opens xls_file and reads 'Service' sheet
|
||||
Returns the list of services data in Request class
|
||||
"""
|
||||
with open_workbook(input_filename) as wb:
|
||||
service_sheet = wb.sheet_by_name('Service')
|
||||
services = list(parse_service_sheet(service_sheet))
|
||||
return services
|
||||
|
||||
|
||||
def parse_service_sheet(service_sheet):
|
||||
def parse_service_sheet(service_sheet) -> Request:
|
||||
""" reads each column according to authorized fieldnames. order is not important.
|
||||
"""
|
||||
logger.debug(f'Validating headers on {service_sheet.name!r}')
|
||||
logger.debug('Validating headers on %r', service_sheet.name)
|
||||
# add a test on field to enable the '' field case that arises when columns on the
|
||||
# right hand side are used as comments or drawing in the excel sheet
|
||||
header = [x.value.strip() for x in service_sheet.row(4)[0:SERVICES_COLUMN]
|
||||
@@ -239,14 +289,52 @@ def parse_service_sheet(service_sheet):
|
||||
'routing: is loose?': 'is_loose', 'path bandwidth': 'path_bandwidth'}
|
||||
try:
|
||||
service_fieldnames = [authorized_fieldnames[e] for e in header]
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
msg = f'Malformed header on Service sheet: {header} field not in {authorized_fieldnames}'
|
||||
raise ValueError(msg)
|
||||
raise ValueError(msg) from e
|
||||
for row in all_rows(service_sheet, start=5):
|
||||
yield Request(**parse_row(row[0:SERVICES_COLUMN], service_fieldnames))
|
||||
|
||||
|
||||
def correct_xls_route_list(network_filename, network, pathreqlist):
|
||||
def check_end_points(pathreq: Request_element, network: DiGraph):
|
||||
"""Raise error if end point is not correct
|
||||
"""
|
||||
transponders = [n.uid for n in network.nodes() if isinstance(n, Transceiver)]
|
||||
if pathreq.source not in transponders:
|
||||
msg = f'Request: {pathreq.request_id}: could not find' +\
|
||||
f' transponder source : {pathreq.source}.'
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
if pathreq.destination not in transponders:
|
||||
msg = f'Request: {pathreq.request_id}: could not find' +\
|
||||
f' transponder destination: {pathreq.destination}.'
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
|
||||
|
||||
def find_node_sugestion(n_id, corresp_roadm, corresp_fused, corresp_ila, network):
|
||||
"""
|
||||
"""
|
||||
roadmtype = [n.uid for n in network.nodes() if isinstance(n, Roadm)]
|
||||
edfatype = [n.uid for n in network.nodes() if isinstance(n, Edfa)]
|
||||
# check that n_id is in the node list, if not find a correspondance name
|
||||
if n_id in roadmtype + edfatype:
|
||||
return [n_id]
|
||||
# checks first roadm, fused, and ila in this order, because ila automatic name
|
||||
# contains roadm names. If it is a fused node, next ila names might be correct
|
||||
# suggestions, especially if following fibers were splitted and ila names
|
||||
# created with the name of the fused node
|
||||
if n_id in corresp_roadm.keys():
|
||||
return corresp_roadm[n_id]
|
||||
if n_id in corresp_fused.keys():
|
||||
return corresp_fused[n_id] + corresp_ila[n_id]
|
||||
if n_id in corresp_ila.keys():
|
||||
return corresp_ila[n_id]
|
||||
return []
|
||||
|
||||
|
||||
def correct_xls_route_list(network_filename: Path, network: DiGraph,
|
||||
pathreqlist: List[Request_element]) -> List[Request_element]:
|
||||
""" prepares the format of route list of nodes to be consistant with nodes names:
|
||||
remove wrong names, find correct names for ila, roadm and fused if the entry was
|
||||
xls.
|
||||
@@ -260,30 +348,17 @@ def correct_xls_route_list(network_filename, network, pathreqlist):
|
||||
corresp_ila, next_node = corresp_next_node(network, corresp_ila, corresp_roadm)
|
||||
# finally correct constraints based on these dict
|
||||
trxfibertype = [n.uid for n in network.nodes() if isinstance(n, (Transceiver, Fiber))]
|
||||
roadmtype = [n.uid for n in network.nodes() if isinstance(n, Roadm)]
|
||||
edfatype = [n.uid for n in network.nodes() if isinstance(n, Edfa)]
|
||||
# TODO there is a problem of identification of fibers in case of parallel
|
||||
# fibers between two adjacent roadms so fiber constraint is not supported
|
||||
transponders = [n.uid for n in network.nodes() if isinstance(n, Transceiver)]
|
||||
for pathreq in pathreqlist:
|
||||
# first check that source and dest are transceivers
|
||||
if pathreq.source not in transponders:
|
||||
msg = f'Request: {pathreq.request_id}: could not find' +\
|
||||
f' transponder source : {pathreq.source}.'
|
||||
raise ServiceError(msg)
|
||||
|
||||
if pathreq.destination not in transponders:
|
||||
msg = f'Request: {pathreq.request_id}: could not find' +\
|
||||
f' transponder destination: {pathreq.destination}.'
|
||||
raise ServiceError(msg)
|
||||
check_end_points(pathreq, network)
|
||||
# silently pop source and dest nodes from the list if they were added by the user as first
|
||||
# and last elem in the constraints respectively. Other positions must lead to an error
|
||||
# caught later on
|
||||
if pathreq.nodes_list and pathreq.source == pathreq.nodes_list[0]:
|
||||
pathreq.loose_list.pop(0)
|
||||
pathreq.nodes_list.pop(0)
|
||||
if pathreq.nodes_list and pathreq.destination == pathreq.nodes_list[-1]:
|
||||
pathreq.loose_list.pop(-1)
|
||||
pathreq.nodes_list.pop(-1)
|
||||
# Then process user defined constraints with respect to automatic namings
|
||||
temp = deepcopy(pathreq)
|
||||
@@ -293,73 +368,56 @@ def correct_xls_route_list(network_filename, network, pathreqlist):
|
||||
# n_id must not be a transceiver and must not be a fiber (non supported, user
|
||||
# can not enter fiber names in excel)
|
||||
if n_id not in trxfibertype:
|
||||
# check that n_id is in the node list, if not find a correspondance name
|
||||
if n_id in roadmtype + edfatype:
|
||||
nodes_suggestion = [n_id]
|
||||
else:
|
||||
# checks first roadm, fused, and ila in this order, because ila automatic name
|
||||
# contain roadm names. If it is a fused node, next ila names might be correct
|
||||
# suggestions, especially if following fibers were splitted and ila names
|
||||
# created with the name of the fused node
|
||||
if n_id in corresp_roadm.keys():
|
||||
nodes_suggestion = corresp_roadm[n_id]
|
||||
elif n_id in corresp_fused.keys():
|
||||
nodes_suggestion = corresp_fused[n_id] + corresp_ila[n_id]
|
||||
elif n_id in corresp_ila.keys():
|
||||
nodes_suggestion = corresp_ila[n_id]
|
||||
nodes_suggestion = find_node_sugestion(n_id, corresp_roadm, corresp_fused, corresp_ila, network)
|
||||
try:
|
||||
if len(nodes_suggestion) > 1:
|
||||
# if there is more than one suggestion, we need to choose the direction
|
||||
# we rely on the next node provided by the user for this purpose
|
||||
new_n = next(n for n in nodes_suggestion
|
||||
if n in next_node
|
||||
and next_node[n] in temp.nodes_list[i:] + [pathreq.destination]
|
||||
and next_node[n] not in temp.nodes_list[:i])
|
||||
elif len(nodes_suggestion) == 1:
|
||||
new_n = nodes_suggestion[0]
|
||||
else:
|
||||
nodes_suggestion = []
|
||||
if nodes_suggestion:
|
||||
try:
|
||||
if len(nodes_suggestion) > 1:
|
||||
# if there is more than one suggestion, we need to choose the direction
|
||||
# we rely on the next node provided by the user for this purpose
|
||||
new_n = next(n for n in nodes_suggestion
|
||||
if n in next_node.keys() and next_node[n]
|
||||
in temp.nodes_list[i:] + [pathreq.destination] and
|
||||
next_node[n] not in temp.nodes_list[:i])
|
||||
else:
|
||||
new_n = nodes_suggestion[0]
|
||||
if new_n != n_id:
|
||||
# warns the user when the correct name is used only in verbose mode,
|
||||
# eg 'a' is a roadm and correct name is 'roadm a' or when there was
|
||||
# too much ambiguity, 'b' is an ila, its name can be:
|
||||
# Edfa0_fiber (a → b)-xx if next node is c or
|
||||
# Edfa0_fiber (c → b)-xx if next node is a
|
||||
msg = f'Request {pathreq.request_id}: Invalid route node specified:' \
|
||||
+ f'\n\t\'{n_id}\', replaced with \'{new_n}\''
|
||||
logger.warning(msg)
|
||||
pathreq.nodes_list[pathreq.nodes_list.index(n_id)] = new_n
|
||||
except StopIteration:
|
||||
# shall not come in this case, unless requested direction does not exist
|
||||
msg = f'Request {pathreq.request_id}: Invalid route specified {n_id}: could' \
|
||||
+ ' not decide on direction, skipped!.\nPlease add a valid' \
|
||||
+ ' direction in constraints (next neighbour node)'
|
||||
logger.warning(msg)
|
||||
pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
else:
|
||||
if temp.loose_list[i] == 'LOOSE':
|
||||
# if no matching can be found in the network just ignore this constraint
|
||||
# if it is a loose constraint
|
||||
# warns the user that this node is not part of the topology
|
||||
msg = f'Request {pathreq.request_id}: Invalid node specified:\n\t\'{n_id}\'' \
|
||||
+ ', could not use it as constraint, skipped!'
|
||||
logger.warning(msg)
|
||||
pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
else:
|
||||
msg = f'Request {pathreq.request_id}: Could not find node:\n\t\'{n_id}\' in network' \
|
||||
if temp.loose == 'LOOSE':
|
||||
# if no matching can be found in the network just ignore this constraint
|
||||
# if it is a loose constraint
|
||||
# warns the user that this node is not part of the topology
|
||||
msg = f'{pathreq.request_id}: Invalid node specified:\n\t\'{n_id}\'' \
|
||||
+ ', could not use it as constraint, skipped!'
|
||||
print(msg)
|
||||
logger.info(msg)
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
continue
|
||||
msg = f'{pathreq.request_id}: Could not find node:\n\t\'{n_id}\' in network' \
|
||||
+ ' topology. Strict constraint can not be applied.'
|
||||
raise ServiceError(msg)
|
||||
if new_n != n_id:
|
||||
# warns the user when the correct name is used only in verbose mode,
|
||||
# eg 'a' is a roadm and correct name is 'roadm a' or when there was
|
||||
# too much ambiguity, 'b' is an ila, its name can be:
|
||||
# "east edfa in b to c", or "west edfa in b to a" if next node is c or
|
||||
# "west edfa in b to c", or "east edfa in b to a" if next node is a
|
||||
msg = f'{pathreq.request_id}: Invalid route node specified:' \
|
||||
+ f'\n\t\'{n_id}\', replaced with \'{new_n}\''
|
||||
logger.info(msg)
|
||||
pathreq.nodes_list[pathreq.nodes_list.index(n_id)] = new_n
|
||||
except StopIteration:
|
||||
# shall not come in this case, unless requested direction does not exist
|
||||
msg = f'{pathreq.request_id}: Invalid route specified {n_id}: could' \
|
||||
+ ' not decide on direction, skipped!.\nPlease add a valid' \
|
||||
+ ' direction in constraints (next neighbour node)'
|
||||
logger.info(msg)
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
else:
|
||||
if temp.loose_list[i] == 'LOOSE':
|
||||
logger.warning(f'Request {pathreq.request_id}: Invalid route node specified:\n\t\'{n_id}\''
|
||||
+ ' type is not supported as constraint with xls network input, skipped!')
|
||||
pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
|
||||
if temp.loose == 'LOOSE':
|
||||
msg = f'{pathreq.request_id}: Invalid route node specified:\n\t\'{n_id}\'' \
|
||||
+ ' type is not supported as constraint with xls network input, skipped!'
|
||||
logger.warning(msg)
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
else:
|
||||
msg = f'Invalid route node specified \n\t\'{n_id}\'' \
|
||||
msg = f'{pathreq.request_id}: Invalid route node specified \n\t\'{n_id}\'' \
|
||||
+ ' type is not supported as constraint with xls network input,' \
|
||||
+ ', Strict constraint can not be applied.'
|
||||
raise ServiceError(msg)
|
||||
|
||||
@@ -24,7 +24,7 @@ from networkx.utils import pairwise
|
||||
from numpy import mean, argmin
|
||||
|
||||
from gnpy.core.elements import Transceiver, Roadm, Edfa, Multiband_amplifier
|
||||
from gnpy.core.utils import lin2db, find_common_range
|
||||
from gnpy.core.utils import lin2db, unique_ordered, find_common_range
|
||||
from gnpy.core.info import create_input_spectral_information, carriers_to_spectral_information, \
|
||||
demuxed_spectral_information, muxed_spectral_information, SpectralInformation
|
||||
from gnpy.core import network as network_module
|
||||
@@ -301,7 +301,9 @@ def compute_constrained_path(network, req):
|
||||
nodes_list = []
|
||||
for node in req.nodes_list[:-1]:
|
||||
nodes_list.append(next(el for el in network if el.uid == node))
|
||||
|
||||
total_path = explicit_path(nodes_list, source, destination, network)
|
||||
if total_path is not None:
|
||||
return total_path
|
||||
try:
|
||||
path_generator = shortest_simple_paths(network, source, destination, weight='weight')
|
||||
total_path = next(path for path in path_generator if ispart(nodes_list, path))
|
||||
@@ -1176,6 +1178,7 @@ def compute_path_with_disjunction(network, equipment, pathreqlist, pathlist, red
|
||||
pathreq.tx_osnr = mode['tx_osnr']
|
||||
pathreq.bit_rate = mode['bit_rate']
|
||||
pathreq.penalties = mode['penalties']
|
||||
pathreq.offset_db = mode['equalization_offset_db']
|
||||
# other blocking reason should not appear at this point
|
||||
except AttributeError:
|
||||
pathreq.baud_rate = mode['baud_rate']
|
||||
@@ -1185,6 +1188,7 @@ def compute_path_with_disjunction(network, equipment, pathreqlist, pathlist, red
|
||||
pathreq.tx_osnr = mode['tx_osnr']
|
||||
pathreq.bit_rate = mode['bit_rate']
|
||||
pathreq.penalties = mode['penalties']
|
||||
pathreq.offset_db = mode['equalization_offset_db']
|
||||
|
||||
# reversed path is needed for correct spectrum assignment
|
||||
reversed_path = find_reversed_path(pathlist[i])
|
||||
@@ -1253,6 +1257,44 @@ def _penalty_msg(total_path, msg, min_ind):
|
||||
return msg
|
||||
|
||||
|
||||
def is_adjacent(oms1, oms2):
|
||||
""" oms1's egress ROADM is oms2's ingress ROADM
|
||||
"""
|
||||
return oms1.el_list[-1] == oms2.el_list[0]
|
||||
|
||||
|
||||
def explicit_path(node_list, source, destination, network):
|
||||
""" if list of nodes leads to adjacent oms, then means that the path is explicit, and no need to compute
|
||||
the function returns the explicit path (including source and destination ROADMs)
|
||||
"""
|
||||
path_oms = []
|
||||
for elem in node_list:
|
||||
if hasattr(elem, 'oms'):
|
||||
path_oms.append(elem.oms)
|
||||
if not path_oms:
|
||||
return None
|
||||
path_oms = unique_ordered(path_oms)
|
||||
try:
|
||||
next_node = next(network.successors(source))
|
||||
source_roadm = next_node if isinstance(next_node, Roadm) else source
|
||||
previous_node = next(network.predecessors(destination))
|
||||
destination_roadm = previous_node if isinstance(previous_node, Roadm) else destination
|
||||
if not (path_oms[0].el_list[0] == source_roadm and path_oms[-1].el_list[-1] == destination_roadm):
|
||||
return None
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
oms0 = path_oms[0]
|
||||
path = [source] + oms0.el_list
|
||||
for oms in path_oms[1:]:
|
||||
if not is_adjacent(oms0, oms):
|
||||
return None
|
||||
oms0 = oms
|
||||
path.extend(oms.el_list)
|
||||
path.append(destination)
|
||||
return unique_ordered(path)
|
||||
|
||||
|
||||
def find_elements_common_range(el_list: list, equipment: dict) -> List[dict]:
|
||||
"""Find the common frequency range of amps of a given list of elements (for example an OMS or a path)
|
||||
If there are no amplifiers in the path, then use the SI
|
||||
|
||||
BIN
tests/data/ila_constraint.xlsx
Executable file
BIN
tests/data/ila_constraint.xlsx
Executable file
Binary file not shown.
Binary file not shown.
@@ -63,7 +63,7 @@
|
||||
{
|
||||
"uid": "roadm Lannion_CAS",
|
||||
"type": "Roadm",
|
||||
"type_variety": "default",
|
||||
"type_variety": "example_detailed_impairments",
|
||||
"params": {
|
||||
"target_pch_out_db": -18.6,
|
||||
"restrictions": {
|
||||
@@ -74,7 +74,14 @@
|
||||
"east edfa in Lannion_CAS to Corlay": -18.6,
|
||||
"east edfa in Lannion_CAS to Morlaix": -23.0,
|
||||
"east edfa in Lannion_CAS to Stbrieuc": -20
|
||||
}
|
||||
},
|
||||
"per_degree_impairments": [
|
||||
{
|
||||
"from_degree": "west edfa in Lannion_CAS to Morlaix",
|
||||
"to_degree": "east edfa in Lannion_CAS to Stbrieuc",
|
||||
"impairment_id": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
|
||||
@@ -62,7 +62,12 @@
|
||||
},
|
||||
{
|
||||
"uid": "roadm Lannion_CAS",
|
||||
"type_variety": "example_detailed_impairments",
|
||||
"params": {
|
||||
"per_degree_impairments": [{
|
||||
"from_degree": "west edfa in Lannion_CAS to Morlaix",
|
||||
"impairment_id": 0,
|
||||
"to_degree": "east edfa in Lannion_CAS to Stbrieuc"}],
|
||||
"per_degree_pch_out_db": {
|
||||
"east edfa in Lannion_CAS to Corlay": -18.6,
|
||||
"east edfa in Lannion_CAS to Morlaix": -23.0
|
||||
@@ -661,7 +666,7 @@
|
||||
"type": "Edfa",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"tilt_target": 0
|
||||
"tilt_target": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -677,7 +682,7 @@
|
||||
"type": "Edfa",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"tilt_target": 0
|
||||
"tilt_target": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -693,7 +698,7 @@
|
||||
"type": "Edfa",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"tilt_target": 0
|
||||
"tilt_target": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -709,7 +714,7 @@
|
||||
"type": "Edfa",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"tilt_target": 0
|
||||
"tilt_target": null
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -727,7 +732,7 @@
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -761,7 +766,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -779,7 +784,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -797,7 +802,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -815,7 +820,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -833,7 +838,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -851,7 +856,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -869,7 +874,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -887,7 +892,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -905,7 +910,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -923,7 +928,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -941,7 +946,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -959,7 +964,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -978,7 +983,7 @@
|
||||
"operational": {
|
||||
"gain_target": 18.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -996,7 +1001,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1014,7 +1019,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1032,7 +1037,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1171,7 +1171,7 @@
|
||||
"operational": {
|
||||
"gain_target": 19.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1190,7 +1190,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1209,7 +1209,7 @@
|
||||
"operational": {
|
||||
"gain_target": 18.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": 0.5
|
||||
}
|
||||
},
|
||||
@@ -1227,7 +1227,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1245,7 +1245,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1263,7 +1263,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1281,7 +1281,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1299,7 +1299,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1318,7 +1318,7 @@
|
||||
"operational": {
|
||||
"gain_target": 18.5,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1336,7 +1336,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1354,7 +1354,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1372,7 +1372,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1391,7 +1391,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1428,7 +1428,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1446,7 +1446,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1464,7 +1464,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1482,7 +1482,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1500,7 +1500,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1518,7 +1518,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1551,7 +1551,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1569,7 +1569,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1587,7 +1587,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1623,7 +1623,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1641,7 +1641,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1659,7 +1659,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1677,7 +1677,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1695,7 +1695,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1713,7 +1713,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1731,7 +1731,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1749,7 +1749,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1768,7 +1768,7 @@
|
||||
"operational": {
|
||||
"gain_target": 18.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1786,7 +1786,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1805,7 +1805,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1823,7 +1823,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1841,7 +1841,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1859,7 +1859,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1877,7 +1877,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1895,7 +1895,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1913,7 +1913,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1931,7 +1931,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1949,7 +1949,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1967,7 +1967,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -1985,7 +1985,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2003,7 +2003,7 @@
|
||||
"operational": {
|
||||
"gain_target": 19.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2021,7 +2021,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2039,7 +2039,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2057,7 +2057,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2075,7 +2075,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2093,7 +2093,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2111,7 +2111,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2129,7 +2129,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2147,7 +2147,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2165,7 +2165,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2183,7 +2183,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2201,7 +2201,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2219,7 +2219,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2237,7 +2237,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2255,7 +2255,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2273,7 +2273,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2291,7 +2291,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2309,7 +2309,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2327,7 +2327,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
@@ -2345,7 +2345,7 @@
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"tilt_target": null,
|
||||
"out_voa": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ WARNING gnpy.core.network:network.py
|
||||
is above user specified amplifier std_low_gain
|
||||
max flat gain: 16dB ; required gain: 21.22dB. Please check amplifier type.
|
||||
|
||||
WARNING gnpy.core.network:network.py
|
||||
WARNING: effective gain in Node east edfa in Stbrieuc to Rennes_STA
|
||||
is below user specified amplifier std_medium_gain
|
||||
min gain: 15dB ; required gain: 12.0dB. Please check amplifier type.
|
||||
|
||||
WARNING gnpy.core.network:network.py
|
||||
WARNING: target gain and power in node west edfa in Rennes_STA to Stbrieuc
|
||||
is beyond all available amplifiers capabilities and/or extended_gain_range:
|
||||
|
||||
@@ -14,6 +14,11 @@ WARNING gnpy.core.network:network.py
|
||||
is above user specified amplifier std_low_gain
|
||||
max flat gain: 16dB ; required gain: 21.18dB. Please check amplifier type.
|
||||
|
||||
WARNING gnpy.core.network:network.py
|
||||
WARNING: effective gain in Node east edfa in Stbrieuc to Rennes_STA
|
||||
is below user specified amplifier std_medium_gain
|
||||
min gain: 15dB ; required gain: 12.0dB. Please check amplifier type.
|
||||
|
||||
WARNING gnpy.core.network:network.py
|
||||
WARNING: target gain and power in node west edfa in Rennes_STA to Stbrieuc
|
||||
is beyond all available amplifiers capabilities and/or extended_gain_range:
|
||||
|
||||
@@ -16,6 +16,7 @@ from gnpy.tools.convert import xls_to_json_data
|
||||
|
||||
TEST_DIR = Path(__file__).parent
|
||||
EQPT_FILENAME = TEST_DIR / 'data/eqpt_config.json'
|
||||
MULTIBAND_EQPT_FILENAME = TEST_DIR / 'data/eqpt_config_multiband.json'
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
|
||||
|
||||
@@ -420,3 +421,167 @@ def test_log_wrong_xlsx(caplog, input_filename, expected_msg):
|
||||
"""
|
||||
_ = xls_to_json_data(input_filename)
|
||||
assert expected_msg in caplog.text
|
||||
|
||||
|
||||
def wrong_configs():
|
||||
wrong_config = [[{
|
||||
"uid": "Edfa1",
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_medium_gain_multiband",
|
||||
"operational": {
|
||||
"gain_target": 21,
|
||||
"delta_p": 3.0,
|
||||
"out_voa": 3.0,
|
||||
"in_voa": 0.0,
|
||||
"tilt_target": 0.0,
|
||||
"f_min": 186.2,
|
||||
"f_max": 190.2
|
||||
}},
|
||||
ParametersError]
|
||||
]
|
||||
wrong_config.append([{
|
||||
"uid": "[83/WR-2-4-SIG=>930/WRT-1-2-SIG]-PhysicalConn/2056",
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"dispersion_per_frequency": {
|
||||
"frequency": [
|
||||
185.49234135667396e12,
|
||||
186.05251641137855e12,
|
||||
188.01312910284463e12,
|
||||
189.99124726477024e12],
|
||||
"value": [
|
||||
1.60e-05,
|
||||
1.67e-05,
|
||||
1.7e-05,
|
||||
1.8e-05]
|
||||
},
|
||||
"loss_coef": 2.85,
|
||||
"length_units": "km",
|
||||
"att_in": 0.0,
|
||||
"con_in": 0.0,
|
||||
"con_out": 0.0
|
||||
}},
|
||||
ParametersError
|
||||
])
|
||||
wrong_config.append([{
|
||||
"uid": "east edfa in Lannion_CAS to Stbrieuc",
|
||||
"metadata": {
|
||||
"location": {
|
||||
"city": "Lannion_CAS",
|
||||
"region": "RLD",
|
||||
"latitude": 2.0,
|
||||
"longitude": 0.0
|
||||
}
|
||||
},
|
||||
"type": "Multiband_amplifier",
|
||||
"type_variety": "std_low_gain_multiband",
|
||||
"amplifiers": [{
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": 0,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
}
|
||||
}, {
|
||||
"type_variety": "std_low_gain_L_ter",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": 1,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 1
|
||||
}
|
||||
}]},
|
||||
ConfigurationError])
|
||||
wrong_config.append([{
|
||||
"uid": "east edfa in Lannion_CAS to Stbrieuc",
|
||||
"metadata": {
|
||||
"location": {
|
||||
"city": "Lannion_CAS",
|
||||
"region": "RLD",
|
||||
"latitude": 2.0,
|
||||
"longitude": 0.0
|
||||
}
|
||||
},
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_low_gain_multiband",
|
||||
"amplifiers": [{
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": 0,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
}
|
||||
}, {
|
||||
"type_variety": "std_low_gain_L",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": 1,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 1
|
||||
}
|
||||
}]},
|
||||
ParametersError])
|
||||
wrong_config.append([{
|
||||
"uid": "east edfa in Lannion_CAS to Stbrieuc",
|
||||
"metadata": {
|
||||
"location": {
|
||||
"city": "Lannion_CAS",
|
||||
"region": "RLD",
|
||||
"latitude": 2.0,
|
||||
"longitude": 0.0
|
||||
}
|
||||
},
|
||||
"type": "Multiband_amplifier",
|
||||
"type_variety": "std_low_gain_multiband",
|
||||
"amplifiers": [{
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": 0,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
}
|
||||
}, {
|
||||
"type_variety": "std_low_gain_L",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": 1,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 1
|
||||
}
|
||||
}, {
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"delta_p": 1,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 1
|
||||
}
|
||||
}]},
|
||||
ParametersError])
|
||||
return wrong_config
|
||||
|
||||
|
||||
@pytest.mark.parametrize('el_config, error_type', wrong_configs())
|
||||
def test_wrong_multiband(el_config, error_type):
|
||||
|
||||
equipment = load_equipment(MULTIBAND_EQPT_FILENAME)
|
||||
fused_config = {
|
||||
"uid": "[83/WR-2-4-SIG=>930/WRT-1-2-SIG]-Tl/9300",
|
||||
"type": "Fused",
|
||||
"params": {
|
||||
"loss": 20
|
||||
}
|
||||
}
|
||||
json_data = {
|
||||
"elements": [
|
||||
el_config,
|
||||
fused_config
|
||||
],
|
||||
"connections": []
|
||||
}
|
||||
with pytest.raises(error_type):
|
||||
_ = network_from_json(json_data, equipment)
|
||||
|
||||
@@ -32,7 +32,7 @@ from gnpy.topology.spectrum_assignment import build_oms_list, pth_assign_spectru
|
||||
from gnpy.tools.convert import convert_file
|
||||
from gnpy.tools.json_io import (load_json, load_network, save_network, load_equipment, requests_from_json,
|
||||
disjunctions_from_json, network_to_json, network_from_json)
|
||||
from gnpy.tools.service_sheet import read_service_sheet, correct_xls_route_list
|
||||
from gnpy.tools.service_sheet import read_service_sheet, correct_xls_route_list, Request_element, Request
|
||||
|
||||
TEST_DIR = Path(__file__).parent
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
@@ -274,54 +274,45 @@ def test_json_response_generation(xls_input, expected_response_file):
|
||||
else:
|
||||
assert expected['response'][i] == response
|
||||
|
||||
# test the correspondance names dict in case of excel input
|
||||
# test that using the created json network still works with excel input
|
||||
# test all configurations of names: trx names, roadm, fused, ila and fiber
|
||||
# as well as splitted case
|
||||
|
||||
# initial network is based on the couple testTopology.xls/ testTopology_auto_design_expected.json
|
||||
# with added constraints to cover more test cases
|
||||
|
||||
|
||||
@pytest.mark.parametrize('source, destination, route_list, hoptype, expected_correction', [
|
||||
('trx Brest_KLA', 'trx Vannes_KBE',
|
||||
('Brest_KLA', 'Vannes_KBE',
|
||||
'roadm Brest_KLA | roadm Lannion_CAS | roadm Lorient_KMA | roadm Vannes_KBE',
|
||||
'STRICT',
|
||||
'no',
|
||||
['roadm Brest_KLA', 'roadm Lannion_CAS', 'roadm Lorient_KMA', 'roadm Vannes_KBE']),
|
||||
('trx Brest_KLA', 'trx Vannes_KBE',
|
||||
('Brest_KLA', 'Vannes_KBE',
|
||||
'trx Brest_KLA | roadm Lannion_CAS | roadm Lorient_KMA | roadm Vannes_KBE',
|
||||
'STRICT',
|
||||
'No',
|
||||
['roadm Lannion_CAS', 'roadm Lorient_KMA', 'roadm Vannes_KBE']),
|
||||
('trx Lannion_CAS', 'trx Rennes_STA', 'trx Rennes_STA', 'LOOSE', []),
|
||||
('trx Lannion_CAS', 'trx Lorient_KMA', 'toto', 'LOOSE', []),
|
||||
('trx Lannion_CAS', 'trx Lorient_KMA', 'toto', 'STRICT', 'Fail'),
|
||||
('trx Lannion_CAS', 'trx Lorient_KMA', 'Corlay | Loudeac | Lorient_KMA', 'LOOSE',
|
||||
('Lannion_CAS', 'Rennes_STA', 'trx Rennes_STA', 'yes', []),
|
||||
('Lannion_CAS', 'Lorient_KMA', 'toto', 'yes', []),
|
||||
('Lannion_CAS', 'Lorient_KMA', 'toto', 'no', 'Fail'),
|
||||
('Lannion_CAS', 'Lorient_KMA', 'Corlay | Loudeac | Lorient_KMA', 'yes',
|
||||
['west fused spans in Corlay', 'west fused spans in Loudeac', 'roadm Lorient_KMA']),
|
||||
('trx Lannion_CAS', 'trx Lorient_KMA', 'Ploermel | Vannes_KBE', 'LOOSE',
|
||||
('Lannion_CAS', 'Lorient_KMA', 'Ploermel | Vannes_KBE', 'yes',
|
||||
['east edfa in Ploermel to Vannes_KBE', 'roadm Vannes_KBE']),
|
||||
('trx Rennes_STA', 'trx Brest_KLA', 'Vannes_KBE | Quimper | Brest_KLA', 'LOOSE',
|
||||
('Rennes_STA', 'Brest_KLA', 'Vannes_KBE | Quimper | Brest_KLA', 'yes',
|
||||
['roadm Vannes_KBE', 'west edfa in Quimper to Lorient_KMA', 'roadm Brest_KLA']),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', 'Brest_KLA | Quimper | Lorient_KMA', 'LOOSE',
|
||||
('Brest_KLA', 'Rennes_STA', 'Brest_KLA | Quimper | Lorient_KMA', 'yes',
|
||||
['roadm Brest_KLA', 'east edfa in Quimper to Lorient_KMA', 'roadm Lorient_KMA']),
|
||||
('Brest_KLA', 'trx Rennes_STA', '', 'LOOSE', 'Fail'),
|
||||
('trx Brest_KLA', 'Rennes_STA', '', 'LOOSE', 'Fail'),
|
||||
('Brest_KLA', 'Rennes_STA', '', 'LOOSE', 'Fail'),
|
||||
('Brest_KLA', 'trx Rennes_STA', '', 'STRICT', 'Fail'),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', 'trx Rennes_STA', 'STRICT', []),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', None, '', []),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', 'Brest_KLA | Quimper | Ploermel', 'LOOSE',
|
||||
('Brest_KLA', 'trx Rennes_STA', '', 'yes', 'Fail'),
|
||||
('trx Brest_KLA', 'Rennes_STA', '', 'yes', 'Fail'),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', '', 'yes', 'Fail'),
|
||||
('Brest_KLA', 'trx Rennes_STA', '', 'no', 'Fail'),
|
||||
('Brest_KLA', 'Rennes_STA', 'trx Rennes_STA', 'no', []),
|
||||
('Brest_KLA', 'Rennes_STA', None, '', []),
|
||||
('Brest_KLA', 'Rennes_STA', 'Brest_KLA | Quimper | Ploermel', 'yes',
|
||||
['roadm Brest_KLA']),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', 'Brest_KLA | Quimper | Ploermel', 'STRICT',
|
||||
('Brest_KLA', 'Rennes_STA', 'Brest_KLA | Quimper | Ploermel', 'no',
|
||||
['roadm Brest_KLA']),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', 'Brest_KLA | trx Quimper', 'LOOSE', ['roadm Brest_KLA']),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', 'Brest_KLA | trx Lannion_CAS', 'LOOSE', ['roadm Brest_KLA']),
|
||||
('trx Brest_KLA', 'trx Rennes_STA', 'Brest_KLA | trx Lannion_CAS', 'STRICT', 'Fail')
|
||||
('Brest_KLA', 'Rennes_STA', 'Brest_KLA | trx Quimper', 'yes', ['roadm Brest_KLA']),
|
||||
('Brest_KLA', 'Rennes_STA', 'Brest_KLA | trx Lannion_CAS', 'yes', ['roadm Brest_KLA']),
|
||||
('Brest_KLA', 'Rennes_STA', 'Brest_KLA | trx Lannion_CAS', 'no', 'Fail')
|
||||
])
|
||||
def test_excel_ila_constraints(source, destination, route_list, hoptype, expected_correction):
|
||||
"""add different kind of constraints to test all correct_route cases"""
|
||||
service_xls_input = DATA_DIR / 'testTopology.xls'
|
||||
network_json_input = DATA_DIR / 'testTopology_auto_design_expected.json'
|
||||
equipment = load_equipment(eqpt_filename)
|
||||
network = load_network(network_json_input, equipment)
|
||||
# increase length of one span to trigger automatic fiber splitting included by autodesign
|
||||
# so that the test also covers this case
|
||||
@@ -331,37 +322,20 @@ def test_excel_ila_constraints(source, destination, route_list, hoptype, expecte
|
||||
p_db = default_si.power_dbm
|
||||
p_total_db = p_db + lin2db(automatic_nch(default_si.f_min, default_si.f_max, default_si.spacing))
|
||||
build_network(network, equipment, p_db, p_total_db)
|
||||
# create params for a request based on input
|
||||
nodes_list = route_list.split(' | ') if route_list is not None else []
|
||||
# create params for a xls request based on input (note that this not the same type as PathRequest)
|
||||
params = {
|
||||
'request_id': '0',
|
||||
'source': source,
|
||||
'bidir': False,
|
||||
'destination': destination,
|
||||
'trx_type': '',
|
||||
'trx_mode': '',
|
||||
'format': '',
|
||||
'spacing': '',
|
||||
'nodes_list': nodes_list,
|
||||
'loose_list': [hoptype for node in nodes_list] if route_list is not None else '',
|
||||
'f_min': 0,
|
||||
'f_max': 0,
|
||||
'baud_rate': 0,
|
||||
'OSNR': None,
|
||||
'bit_rate': None,
|
||||
'cost': None,
|
||||
'roll_off': 0,
|
||||
'tx_osnr': 0,
|
||||
'tx_power': 0,
|
||||
'penalties': None,
|
||||
'min_spacing': None,
|
||||
'trx_type': 'Voyager',
|
||||
'spacing': 50,
|
||||
'nodes_list': route_list,
|
||||
'is_loose': hoptype,
|
||||
'nb_channel': 0,
|
||||
'power': 0,
|
||||
'path_bandwidth': 0,
|
||||
'effective_freq_slot': None,
|
||||
'equalization_offset_db': 0
|
||||
}
|
||||
request = PathRequest(**params)
|
||||
request = Request_element(Request(**params), equipment, False)
|
||||
|
||||
if expected_correction != 'Fail':
|
||||
[request] = correct_xls_route_list(service_xls_input, network, [request])
|
||||
@@ -371,6 +345,55 @@ def test_excel_ila_constraints(source, destination, route_list, hoptype, expecte
|
||||
[request] = correct_xls_route_list(service_xls_input, network, [request])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('route_list, hoptype, expected_amp_route', [
|
||||
('node1 | siteE | node2', 'no',
|
||||
['roadm node1', 'west edfa in siteE', 'roadm node2']),
|
||||
('node2 | siteE | node1', 'no',
|
||||
['roadm node2', 'east edfa in siteE', 'roadm node1']),
|
||||
('node1 | siteF | node2', 'no',
|
||||
['roadm node1', 'west edfa in siteF', 'roadm node2']),
|
||||
('node1 | siteA | siteB', 'yes',
|
||||
['roadm node1', 'west edfa in siteA']),
|
||||
('node1 | siteA | siteB | node2', 'yes',
|
||||
['roadm node1', 'west edfa in siteA', 'west edfa in siteB', 'roadm node2']),
|
||||
('node1 | siteC | node2', 'yes',
|
||||
['roadm node1', 'east edfa in siteC', 'roadm node2']),
|
||||
('node1 | siteD | node2', 'no',
|
||||
['roadm node1', 'west edfa in siteD to node1', 'roadm node2']),
|
||||
('roadm node1 | Edfa_booster_roadm node1_to_fiber (node1 → siteE)-CABLES#19 | west edfa in siteE | node2',
|
||||
'no',
|
||||
['roadm node1', 'Edfa_booster_roadm node1_to_fiber (node1 → siteE)-CABLES#19',
|
||||
'west edfa in siteE', 'roadm node2'])])
|
||||
def test_excel_ila_constraints2(route_list, hoptype, expected_amp_route):
|
||||
"""Check different cases for ILA constraints definition
|
||||
"""
|
||||
network_xls_input = DATA_DIR / 'ila_constraint.xlsx'
|
||||
network = load_network(network_xls_input, equipment)
|
||||
add_missing_elements_in_network(network, equipment)
|
||||
default_si = equipment['SI']['default']
|
||||
p_db = default_si.power_dbm
|
||||
p_total_db = p_db + lin2db(automatic_nch(default_si.f_min, default_si.f_max, default_si.spacing))
|
||||
build_network(network, equipment, p_db, p_total_db)
|
||||
# create params for a request based on input
|
||||
|
||||
params = {
|
||||
'request_id': '0',
|
||||
'source': 'node1',
|
||||
'destination': 'node2',
|
||||
'trx_type': 'Voyager',
|
||||
'mode': None,
|
||||
'spacing': 50,
|
||||
'nodes_list': route_list,
|
||||
'is_loose': hoptype,
|
||||
'nb_channel': 80,
|
||||
'power': 0,
|
||||
'path_bandwidth': 100,
|
||||
}
|
||||
request = Request_element(Request(**params), equipment, False)
|
||||
[request] = correct_xls_route_list(network_xls_input, network, [request])
|
||||
assert request.nodes_list == expected_amp_route
|
||||
|
||||
|
||||
def setup_per_degree(case):
|
||||
"""common setup for degree: returns the dict network for different cases"""
|
||||
json_network = load_json(DATA_DIR / 'testTopology_expected.json')
|
||||
|
||||
136
tests/test_path_computation_functions.py
Normal file
136
tests/test_path_computation_functions.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
# Module name: test_path_computation_functions.py
|
||||
# Version:
|
||||
# License: BSD 3-Clause Licence
|
||||
# Copyright (c) 2018, Telecom Infra Project
|
||||
|
||||
"""
|
||||
@author: esther.lerouzic
|
||||
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from gnpy.core.network import build_network
|
||||
from gnpy.core.utils import lin2db, automatic_nch
|
||||
from gnpy.topology.request import explicit_path
|
||||
from gnpy.topology.spectrum_assignment import build_oms_list
|
||||
from gnpy.tools.json_io import load_equipment, load_network, requests_from_json
|
||||
|
||||
|
||||
TEST_DIR = Path(__file__).parent
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
EQPT_FILENAME = DATA_DIR / 'eqpt_config.json'
|
||||
NETWORK_FILENAME = DATA_DIR / 'testTopology_auto_design_expected.json'
|
||||
SERVICE_FILENAME = DATA_DIR / 'testTopology_services_expected.json'
|
||||
equipment = load_equipment(EQPT_FILENAME)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def setup_without_oms():
|
||||
""" common setup for tests: builds network, equipment and oms only once
|
||||
"""
|
||||
network = load_network(NETWORK_FILENAME, equipment)
|
||||
spectrum = equipment['SI']['default']
|
||||
p_db = spectrum.power_dbm
|
||||
p_total_db = p_db + lin2db(automatic_nch(spectrum.f_min, spectrum.f_max, spectrum.spacing))
|
||||
build_network(network, equipment, p_db, p_total_db)
|
||||
return network
|
||||
|
||||
|
||||
def some_request(explicit_route):
|
||||
"""Create a request with an explicit route
|
||||
"""
|
||||
route = {
|
||||
"route-object-include-exclude": [
|
||||
{
|
||||
"explicit-route-usage": "route-include-ero",
|
||||
"index": i,
|
||||
"num-unnum-hop": {
|
||||
"node-id": node_id,
|
||||
"link-tp-id": "link-tp-id is not used",
|
||||
"hop-type": "STRICT"
|
||||
}
|
||||
} for i, node_id in enumerate(explicit_route)
|
||||
]
|
||||
}
|
||||
return {
|
||||
"path-request": [{
|
||||
"request-id": "2",
|
||||
"source": explicit_route[0],
|
||||
"destination": explicit_route[-1],
|
||||
"src-tp-id": explicit_route[0],
|
||||
"dst-tp-id": explicit_route[-1],
|
||||
"bidirectional": False,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
"trx_type": "Voyager",
|
||||
"trx_mode": "mode 1",
|
||||
"spacing": 75000000000.0,
|
||||
"path_bandwidth": 100000000000.0
|
||||
}
|
||||
},
|
||||
"explicit-route-objects": route}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('setup', ["with_oms", "without_oms"])
|
||||
@pytest.mark.parametrize('explicit_route, expected_path', [
|
||||
(['trx Brest_KLA', 'trx Vannes_KBE'], None),
|
||||
# path contains one element per oms
|
||||
(['trx Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'trx Lannion_CAS'],
|
||||
['trx Brest_KLA', 'roadm Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'fiber (Brest_KLA → Morlaix)-F060',
|
||||
'east fused spans in Morlaix', 'fiber (Morlaix → Lannion_CAS)-F059', 'west edfa in Lannion_CAS to Morlaix',
|
||||
'roadm Lannion_CAS', 'trx Lannion_CAS']),
|
||||
# path contains several elements per oms
|
||||
(['trx Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'west edfa in Lannion_CAS to Morlaix',
|
||||
'roadm Lannion_CAS', 'trx Lannion_CAS'],
|
||||
['trx Brest_KLA', 'roadm Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'fiber (Brest_KLA → Morlaix)-F060',
|
||||
'east fused spans in Morlaix', 'fiber (Morlaix → Lannion_CAS)-F059', 'west edfa in Lannion_CAS to Morlaix',
|
||||
'roadm Lannion_CAS', 'trx Lannion_CAS']),
|
||||
# path contains all elements
|
||||
(['trx Brest_KLA', 'roadm Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'fiber (Brest_KLA → Morlaix)-F060',
|
||||
'east fused spans in Morlaix', 'fiber (Morlaix → Lannion_CAS)-F059', 'west edfa in Lannion_CAS to Morlaix',
|
||||
'roadm Lannion_CAS', 'trx Lannion_CAS'],
|
||||
['trx Brest_KLA', 'roadm Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'fiber (Brest_KLA → Morlaix)-F060',
|
||||
'east fused spans in Morlaix', 'fiber (Morlaix → Lannion_CAS)-F059', 'west edfa in Lannion_CAS to Morlaix',
|
||||
'roadm Lannion_CAS', 'trx Lannion_CAS']),
|
||||
# path conteains element for only 1 oms (2 oms path)
|
||||
(['trx Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'trx Rennes_STA'], None),
|
||||
# path contains roadm edges for all OMS, but no element of the OMS
|
||||
(['trx Brest_KLA', 'roadm Brest_KLA', 'roadm Lannion_CAS', 'trx Lannion_CAS'], None),
|
||||
# path contains one element for all 3 OMS
|
||||
(['trx Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'east edfa in Lannion_CAS to Corlay',
|
||||
'east edfa in Lorient_KMA to Vannes_KBE', 'trx Vannes_KBE'],
|
||||
['trx Brest_KLA', 'roadm Brest_KLA', 'east edfa in Brest_KLA to Morlaix', 'fiber (Brest_KLA → Morlaix)-F060',
|
||||
'east fused spans in Morlaix', 'fiber (Morlaix → Lannion_CAS)-F059', 'west edfa in Lannion_CAS to Morlaix',
|
||||
'roadm Lannion_CAS', 'east edfa in Lannion_CAS to Corlay', 'fiber (Lannion_CAS → Corlay)-F061',
|
||||
'west fused spans in Corlay', 'fiber (Corlay → Loudeac)-F010', 'west fused spans in Loudeac',
|
||||
'fiber (Loudeac → Lorient_KMA)-F054', 'west edfa in Lorient_KMA to Loudeac', 'roadm Lorient_KMA',
|
||||
'east edfa in Lorient_KMA to Vannes_KBE', 'fiber (Lorient_KMA → Vannes_KBE)-F055',
|
||||
'west edfa in Vannes_KBE to Lorient_KMA', 'roadm Vannes_KBE', 'trx Vannes_KBE'])])
|
||||
def test_explicit_path(setup, setup_without_oms, explicit_route, expected_path):
|
||||
"""tests that explicit path correctly returns the full path if it is possible else that it returns None
|
||||
"""
|
||||
network = setup_without_oms
|
||||
if setup == "with_oms":
|
||||
# OMS are initiated in elements, so that explicit path can be verified
|
||||
build_oms_list(network, equipment)
|
||||
else:
|
||||
# OMS are not initiated, explicit path can not be computed.
|
||||
expected_path = None
|
||||
json_data = some_request(explicit_route)
|
||||
[req] = requests_from_json(json_data, equipment)
|
||||
|
||||
node_list = []
|
||||
for node in req.nodes_list:
|
||||
node_list.append(next(el for el in network if el.uid == node))
|
||||
source = node_list[0]
|
||||
destination = node_list[-1]
|
||||
if expected_path is None:
|
||||
assert explicit_path(node_list, source, destination, network) is None
|
||||
else:
|
||||
actual_path = [e.uid for e in explicit_path(node_list, source, destination, network)]
|
||||
assert actual_path == expected_path
|
||||
Reference in New Issue
Block a user