Restrictions on auto-adding amplifiers into ROADMs

This feature is intended to support designs such as OpenROADM where the
line degree integrates a specific preamp/booster pair. In that case, it
does not make sense for our autodesign to "pick an amplifier". The
restrictions can be activated by:

- Listing them in `eqpt_config.json`, so that they are effective for all
ROADM instances.
- On a per-ROADM basis within the Excel sheet or the JSON definitions.

Restrictions apply to an entire ROADM as a whole, not to the individual
degrees.

If a per-degree exception is needed, the amplifier of this degree can be
defined in the equipment sheet or in the network definition.

If no booster amplifier should be placed on a degree, use the `Fused`
node in place of an amplifier.

Signed-off-by: Esther Le Rouzic <esther.lerouzic@orange.com>
Co-authored-by: Jan Kundrát <jan.kundrat@telecominfraproject.com>
This commit is contained in:
Esther Le Rouzic
2019-05-31 15:54:07 +02:00
committed by Jan Kundrát
parent 22acd88d44
commit d94dc51d88
18 changed files with 1128 additions and 259 deletions

View File

@@ -19,8 +19,8 @@ In order to work the excel file MUST contain at least 2 sheets:
Nodes sheet
-----------
Nodes sheet contains seven columns.
Each line represents a 'node' (ROADM site or an in line amplifier site ILA)::
Nodes sheet contains nine columns.
Each line represents a 'node' (ROADM site or an in line amplifier site ILA or a Fused)::
City (Mandatory) ; State ; Country ; Region ; Latitude ; Longitude ; Type
@@ -38,6 +38,9 @@ Each line represents a 'node' (ROADM site or an in line amplifier site ILA)::
- *Longitude*, *Latitude* are not mandatory. If filled they should contain numbers.
- **Booster_restriction** and **Preamp_restriction** are not mandatory.
If used, they must contain one or several amplifier type_variety names separated by ' | '. This information is used to restrict types of amplifiers used in a ROADM node during autodesign. If a ROADM booster or preamp is already specified in the Eqpt sheet , the field is ignored. The field is also ignored if the node is not a ROADM node.
**There MUST NOT be empty line(s) between two nodes lines**
@@ -166,6 +169,7 @@ This generates a text file meshTopologyExampleV2_eqt_sheet.txt whose content ca
- **amp type** is not mandatory.
If filled it must contain types listed in `eqpt_config.json <examples/eqpt_config.json>`_ in "Edfa" list "type_variety".
If not filled it takes "std_medium_gain" as default value.
If filled with fused, a fused element with 0.0 dB loss will be placed instead of an amplifier. This might be used to avoid booster amplifier on a ROADM direction.
- **amp_gain** is not mandatory. It is the value to be set on the amplifier (in dB).
If not filled, it will be determined with design rules in the convert.py file.

View File

@@ -408,10 +408,15 @@ existing parameters:
+--------------------------+-----------+---------------------------------------------+
| ``add_drop_osnr`` | (number) | OSNR contribution from the add/drop ports |
+--------------------------+-----------+---------------------------------------------+
| ``restrictions`` | (strings) | Authorized type_variety of amplifier for |
| | | booster or preamp. |
| | | Listed type_variety MUST be defined in the |
| | | Edfa catalog. |
| ``restrictions`` | (dict of | If non-empty, keys ``preamp_variety_list`` |
| | strings) | and ``booster_variety_list`` represent |
| | | list of ``type_variety`` amplifiers which |
| | | are allowed for auto-design within ROADM's |
| | | line degrees. |
| | | |
| | | If no booster should be placed on a degree, |
| | | insert a ``Fused`` node on the degree |
| | | output. |
+--------------------------+-----------+---------------------------------------------+
The ``SpectralInformation`` object can be configured as follows. The user can

View File

@@ -177,8 +177,8 @@
"target_pch_out_db": -20,
"add_drop_osnr": 38,
"restrictions": {
"preamp_variety_list":["low_gain_preamp", "high_gain_preamp"],
"booster_variety_list":["std_booster"]
"preamp_variety_list":[],
"booster_variety_list":[]
}
}],
"SI":[{

View File

@@ -681,25 +681,6 @@
"out_voa": null
}
},
{
"uid": "east edfa in Lorient_KMA to Vannes_KBE",
"metadata": {
"location": {
"city": "Lorient_KMA",
"region": "RLD",
"latitude": 2.0,
"longitude": 3.0
}
},
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": null,
"delta_p": 1.0,
"tilt_target": 0,
"out_voa": null
}
},
{
"uid": "east edfa in Lannion_CAS to Stbrieuc",
"metadata": {
@@ -1041,6 +1022,21 @@
"tilt_target": 0,
"out_voa": null
}
},
{
"uid": "east edfa in Lorient_KMA to Vannes_KBE",
"metadata": {
"location": {
"city": "Lorient_KMA",
"region": "RLD",
"latitude": 2.0,
"longitude": 3.0
}
},
"type": "Fused",
"params": {
"loss": 0
}
}
],
"connections": [

Binary file not shown.

View File

@@ -31,6 +31,7 @@ from itertools import chain
from json import dumps
from pathlib import Path
from difflib import get_close_matches
from gnpy.core.utils import silent_remove
import time
all_rows = lambda sh, start=0: (sh.row(x) for x in range(start, sh.nrows))
@@ -54,7 +55,9 @@ class Node(object):
'region': '',
'latitude': 0,
'longitude': 0,
'node_type': 'ILA'
'node_type': 'ILA',
'booster_restriction' : '',
'preamp_restriction' : ''
}
class Link(object):
@@ -235,7 +238,6 @@ def sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city):
def convert_file(input_filename, names_matching=False, filter_region=[]):
nodes, links, eqpts = parse_excel(input_filename)
if filter_region:
nodes = [n for n in nodes if n.region.lower() in filter_region]
cities = {n.city for n in nodes}
@@ -244,10 +246,8 @@ def convert_file(input_filename, names_matching=False, 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}
#create matching dictionary for node name mismatch analysis
cities = {''.join(c.strip() for c in n.city.split('C+L')).lower(): n.city for n in nodes}
@@ -298,7 +298,22 @@ def convert_file(input_filename, names_matching=False, filter_region=[]):
'latitude': x.latitude,
'longitude': x.longitude}},
'type': 'Roadm'}
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'] +
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm' \
and x.booster_restriction == '' and x.preamp_restriction == ''] +
[{'uid': f'roadm {x.city}',
'params' : {
'restrictions': {
'preamp_variety_list': silent_remove(x.preamp_restriction.split(' | '),''),
'booster_variety_list': silent_remove(x.booster_restriction.split(' | '),'')
}
},
'metadata': {'location': {'city': x.city,
'region': x.region,
'latitude': x.latitude,
'longitude': x.longitude}},
'type': 'Roadm'}
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm' and \
(x.booster_restriction != '' or x.preamp_restriction != '')] +
[{'uid': f'west fused spans in {x.city}',
'metadata': {'location': {'city': x.city,
'region': x.region,
@@ -348,8 +363,9 @@ def convert_file(input_filename, names_matching=False, filter_region=[]):
'delta_p': e.east_amp_dp,
'tilt_target': e.east_tilt,
'out_voa' : e.east_att_out}
}
for e in eqpts if e.east_amp_type.lower() != ''] +
}
for e in eqpts if (e.east_amp_type.lower() != '' and \
e.east_amp_type.lower() != 'fused')] +
[{'uid': f'west edfa in {e.from_city} to {e.to_city}',
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
'region': nodes_by_city[e.from_city].region,
@@ -361,8 +377,31 @@ def convert_file(input_filename, names_matching=False, filter_region=[]):
'delta_p': e.west_amp_dp,
'tilt_target': e.west_tilt,
'out_voa' : e.west_att_out}
}
for e in eqpts if e.west_amp_type.lower() != ''],
}
for e in eqpts if (e.west_amp_type.lower() != '' and \
e.west_amp_type.lower() != 'fused')] +
# fused edfa variety is a hack to indicate that there should not be
# booster amplifier out the roadm.
# If user specifies ILA in Nodes sheet and fused in Eqpt sheet, then assumes that
# this is a fused nodes.
[{'uid': f'east edfa in {e.from_city} to {e.to_city}',
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
'region': nodes_by_city[e.from_city].region,
'latitude': nodes_by_city[e.from_city].latitude,
'longitude': nodes_by_city[e.from_city].longitude}},
'type': 'Fused',
'params': {'loss': 0}
}
for e in eqpts if e.east_amp_type.lower() == 'fused'] +
[{'uid': f'west edfa in {e.from_city} to {e.to_city}',
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
'region': nodes_by_city[e.from_city].region,
'latitude': nodes_by_city[e.from_city].latitude,
'longitude': nodes_by_city[e.from_city].longitude}},
'type': 'Fused',
'params': {'loss': 0}
}
for e in eqpts if e.west_amp_type.lower() == 'fused'],
'connections':
list(chain.from_iterable([eqpt_connection_by_city(n.city)
for n in nodes]))
@@ -414,7 +453,9 @@ def parse_excel(input_filename):
'Region': 'region',
'Latitude': 'latitude',
'Longitude': 'longitude',
'Type': 'node_type'
'Type': 'node_type',
'Booster_restriction': 'booster_restriction',
'Preamp_restriction': 'preamp_restriction'
}
eqpt_headers = \
{ 'Node A': 'from_city',
@@ -571,7 +612,7 @@ def midpoint(city_a, city_b):
#output_json_file_name = 'coronet_conus_example.json'
#TODO get column size automatically from tupple size
NODES_COLUMN = 8
NODES_COLUMN = 10
NODES_LINE = 4
LINKS_COLUMN = 16
LINKS_LINE = 3

View File

@@ -117,7 +117,7 @@ class Transceiver(Node):
self._calc_snr(spectral_info)
return spectral_info
RoadmParams = namedtuple('RoadmParams', 'target_pch_out_db add_drop_osnr')
RoadmParams = namedtuple('RoadmParams', 'target_pch_out_db add_drop_osnr restrictions')
class Roadm(Node):
def __init__(self, *args, params, **kwargs):
@@ -126,15 +126,19 @@ class Roadm(Node):
self.effective_loss = None
self.effective_pch_out_db = self.params.target_pch_out_db
self.passive = True
self.restrictions = self.params.restrictions
@property
def to_json(self):
return {'uid' : self.uid,
'type' : type(self).__name__,
'params' : {'target_pch_out_db' : self.effective_pch_out_db},
'params' : {
'target_pch_out_db' : self.effective_pch_out_db,
'restrictions' : self.restrictions
},
'metadata' : {
'location': self.metadata['location']._asdict()
}
}
}
def __repr__(self):
@@ -186,6 +190,9 @@ class Fused(Node):
def to_json(self):
return {'uid' : self.uid,
'type' : type(self).__name__,
'params' :{
'loss': self.loss
},
'metadata' : {
'location': self.metadata['location']._asdict()
}

View File

@@ -27,14 +27,14 @@ Model_dual_stage = namedtuple('Model_dual_stage', 'preamp_variety booster_variet
class common:
def update_attr(self, default_values, kwargs, name):
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))
if k not in clean_kwargs and name != 'Amp' :
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))
if k not in clean_kwargs and name != 'Amp':
print(f'\x1b[1;31;40m'+
f'\n WARNING missing {k} attribute in eqpt_config.json[{name}]'
f'\n default value is {k} = {v}'
+ '\x1b[0m')
f'\n WARNING missing {k} attribute in eqpt_config.json[{name}]'+
f'\n default value is {k} = {v}'+
f'\x1b[0m')
time.sleep(1)
class SI(common):
@@ -42,13 +42,13 @@ class SI(common):
{
"f_min": 191.35e12,
"f_max": 196.1e12,
"baud_rate": 32e9,
"baud_rate": 32e9,
"spacing": 50e9,
"power_dbm": 0,
"power_range_db": [0,0,0.5],
"power_range_db": [0, 0, 0.5],
"roll_off": 0.15,
"tx_osnr": 45,
"sys_margins": 0
"sys_margins": 0
}
def __init__(self, **kwargs):
@@ -69,7 +69,7 @@ class Span(common):
'con_in': 0,
'con_out': 0
}
def __init__(self, **kwargs):
self.update_attr(self.default_values, kwargs, 'Span')
@@ -77,8 +77,12 @@ class Roadm(common):
default_values = \
{
'target_pch_out_db': -17,
'add_drop_osnr': 100
}
'add_drop_osnr': 100,
'restrictions': {
'preamp_variety_list':[],
'booster_variety_list':[]
}
}
def __init__(self, **kwargs):
self.update_attr(self.default_values, kwargs, 'Roadm')
@@ -134,7 +138,7 @@ class Amp(common):
config = Path(filename).parent / 'default_edfa_config.json'
type_variety = kwargs['type_variety']
type_def = kwargs.get('type_def', 'variable_gain') #default compatibility with older json eqpt files
type_def = kwargs.get('type_def', 'variable_gain') # default compatibility with older json eqpt files
nf_def = None
dual_stage_def = None
@@ -180,7 +184,7 @@ class Amp(common):
with open(config, encoding='utf-8') as f:
json_data = load(f)
return cls(**{**kwargs, **json_data,
return cls(**{**kwargs, **json_data,
'nf_model': nf_def, 'dual_stage_model': dual_stage_def})
@@ -247,7 +251,7 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
"""return the trx and SI parameters from eqpt_config for a given type_variety and mode (ie format)"""
trx_params = {}
default_si_data = equipment['SI']['default']
try:
trxs = equipment['Transceiver']
#if called from path_requests_run.py, trx_mode is filled with None when not specified by user
@@ -264,14 +268,14 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
f'has baud rate: {trx_params["baud_rate"]*1e-9} GHz greater than min_spacing {trx_params["min_spacing"]*1e-9}.')
else:
mode_params = {"format": "undetermined",
"baud_rate": None,
"OSNR": None,
"bit_rate": None,
"roll_off": None,
"tx_osnr":None,
"min_spacing":None,
"cost":None}
trx_params = {**mode_params}
"baud_rate": None,
"OSNR": None,
"bit_rate": None,
"roll_off": None,
"tx_osnr":None,
"min_spacing":None,
"cost":None}
trx_params = {**mode_params}
trx_params['f_min'] = equipment['Transceiver'][trx_type_variety].frequency['min']
trx_params['f_max'] = equipment['Transceiver'][trx_type_variety].frequency['max']
@@ -298,7 +302,7 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
nch = automatic_nch(trx_params['f_min'], trx_params['f_max'], trx_params['spacing'])
trx_params['nb_channel'] = nch
print(f'There are {nch} channels propagating')
trx_params['power'] = db2lin(default_si_data.power_dbm)*1e-3
return trx_params
@@ -306,8 +310,8 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
def automatic_spacing(baud_rate):
"""return the min possible channel spacing for a given baud rate"""
# TODO : this should parametrized in a cfg file
spacing_list = [(33e9,37.5e9), (38e9,50e9), (50e9,62.5e9), (67e9,75e9), (92e9,100e9)] #list of possible tuples
#[(max_baud_rate, spacing_for_this_baud_rate)]
# 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 automatic_nch(f_min, f_max, spacing):
@@ -333,18 +337,28 @@ def update_dual_stage(equipment):
if edfa.type_def == 'dual_stage':
edfa_preamp = edfa_dict[edfa.dual_stage_model.preamp_variety]
edfa_booster = edfa_dict[edfa.dual_stage_model.booster_variety]
for k,v in edfa_preamp.__dict__.items():
attr_k = 'preamp_'+k
setattr(edfa, attr_k, v)
for k,v in edfa_booster.__dict__.items():
attr_k = 'booster_'+k
setattr(edfa, attr_k, v)
for key, value in edfa_preamp.__dict__.items():
attr_k = 'preamp_' + key
setattr(edfa, attr_k, value)
for key, value in edfa_booster.__dict__.items():
attr_k = 'booster_' + key
setattr(edfa, attr_k, value)
edfa.p_max = edfa_booster.p_max
edfa.gain_flatmax = edfa_booster.gain_flatmax + edfa_preamp.gain_flatmax
if edfa.gain_min < edfa_preamp.gain_min:
raise EquipmentConfigError(f'Dual stage {edfa.type_variety} min gain is lower than its preamp min gain')
return equipment
def roadm_restrictions_sanity_check(equipment):
""" verifies that booster and preamp restrictions specified in roadm equipment are listed
in the edfa.
"""
restrictions = equipment['Roadm']['default'].restrictions['booster_variety_list'] + \
equipment['Roadm']['default'].restrictions['preamp_variety_list']
for amp_name in restrictions:
if amp_name not in equipment['Edfa']:
raise EquipmentConfigError(f'ROADM restriction {amp_name} does not refer to a defined EDFA name')
def equipment_from_json(json_data, filename):
"""build global dictionnary eqpt_library that stores all eqpt characteristics:
edfa type type_variety, fiber type_variety
@@ -359,11 +373,12 @@ def equipment_from_json(json_data, filename):
equipment[key] = {}
typ = globals()[key]
for entry in entries:
subkey = entry.get('type_variety', 'default')
subkey = entry.get('type_variety', 'default')
if key == 'Edfa':
equipment[key][subkey] = Amp.from_json(filename, **entry)
else:
else:
equipment[key][subkey] = typ(**entry)
equipment = update_trx_osnr(equipment)
equipment = update_dual_stage(equipment)
roadm_restrictions_sanity_check(equipment)
return equipment

View File

@@ -20,7 +20,8 @@ from gnpy.core.elements import Fiber, Edfa, Transceiver, Roadm, Fused
from gnpy.core.equipment import edfa_nf
from gnpy.core.exceptions import ConfigurationError, NetworkTopologyError
from gnpy.core.units import UNITS
from gnpy.core.utils import load_json, save_json, round2float, db2lin, lin2db
from gnpy.core.utils import (load_json, save_json, round2float, db2lin,
merge_amplifier_restrictions)
from collections import namedtuple
logger = getLogger(__name__)
@@ -50,10 +51,12 @@ def network_from_json(json_data, equipment):
for el_config in json_data['elements']:
typ = el_config.pop('type')
variety = el_config.pop('type_variety', 'default')
if typ in equipment and variety in equipment[typ]:
if typ in equipment and variety in equipment[typ]:
extra_params = equipment[typ][variety]
el_config.setdefault('params', {}).update(extra_params.__dict__)
elif typ in ['Edfa', 'Fiber']: #catch it now because the code will crash later!
temp = el_config.setdefault('params', {})
temp = merge_amplifier_restrictions(temp, extra_params.__dict__)
el_config['params'] = temp
elif typ in ['Edfa', 'Fiber']: # catch it now because the code will crash later!
raise ConfigurationError(f'The {typ} of variety type {variety} was not recognized:'
'\nplease check it is properly defined in the eqpt_config json file')
cls = getattr(elements, typ)
@@ -65,10 +68,8 @@ def network_from_json(json_data, equipment):
for cx in json_data['connections']:
from_node, to_node = cx['from_node'], cx['to_node']
try:
if isinstance(nodes[from_node], Fiber):
if isinstance(nodes[from_node], Fiber):
edge_length = nodes[from_node].params.length
# print(from_node)
# print(edge_length)
else:
edge_length = 0.01
g.add_edge(nodes[from_node], nodes[to_node], weight = edge_length)
@@ -90,21 +91,27 @@ def network_to_json(network):
data.update(connections)
return data
def select_edfa(raman_allowed, gain_target, power_target, equipment, uid):
def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restrictions=None):
"""amplifer selection algorithm
@Orange Jean-Luc Augé
"""
Edfa_list = namedtuple('Edfa_list', 'variety power gain_min nf')
TARGET_EXTENDED_GAIN = equipment['Span']['default'].target_extended_gain
edfa_dict = equipment['Edfa']
# for roadm restriction only: create a dict including not allowed for design amps
# because main use case is to have specific radm amp which are not allowed for ILA
# with the auto design
edfa_dict = {name: amp for (name, amp) in equipment['Edfa'].items()
if restrictions is None or name in restrictions}
pin = power_target - gain_target
#create 2 list of available amplifiers with relevant attributs for their selection
# create 2 list of available amplifiers with relevant attributes for their selection
#edfa list with :
#extended gain min allowance of 3dB: could be parametrized, but a bit complex
#extended gain max allowance TARGET_EXTENDED_GAIN is coming from eqpt_config.json
#power attribut include power AND gain limitations
# edfa list with:
# extended gain min allowance of 3dB: could be parametrized, but a bit complex
# extended gain max allowance TARGET_EXTENDED_GAIN is coming from eqpt_config.json
# power attribut include power AND gain limitations
edfa_list = [Edfa_list(
variety=edfa_variety,
power=min(
@@ -119,7 +126,7 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid):
-edfa.gain_min,
nf=edfa_nf(gain_target, edfa_variety, equipment)) \
for edfa_variety, edfa in edfa_dict.items()
if (edfa.allowed_for_design and not edfa.raman)]
if ((edfa.allowed_for_design or restrictions is not None) and not edfa.raman)]
#consider a Raman list because of different gain_min requirement:
#do not allow extended gain min for Raman
@@ -326,7 +333,7 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
else: #gain mode with effective_gain
gain_target = node.effective_gain
dp = prev_dp - node_loss + gain_target
#print(node.delta_p, dp, gain_target)
power_target = pref_total_db + dp
raman_allowed = False
@@ -335,9 +342,24 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
equipment['Span']['default'].max_fiber_lineic_loss_for_raman
raman_allowed = prev_node.params.loss_coef < max_fiber_lineic_loss_for_raman
if node.params.type_variety == '' :
# implementation of restrictions on roadm boosters
if isinstance(prev_node,Roadm):
if prev_node.restrictions['booster_variety_list']:
restrictions = prev_node.restrictions['booster_variety_list']
else:
restrictions = None
elif isinstance(next_node,Roadm):
# implementation of restrictions on roadm preamp
if next_node.restrictions['preamp_variety_list']:
restrictions = next_node.restrictions['preamp_variety_list']
else:
restrictions = None
else:
restrictions = None
if node.params.type_variety == '':
edfa_variety, power_reduction = select_edfa(raman_allowed,
gain_target, power_target, equipment, node.uid)
gain_target, power_target, equipment, node.uid, restrictions)
extra_params = equipment['Edfa'][edfa_variety]
node.params.update_params(extra_params.__dict__)
dp += power_reduction
@@ -351,7 +373,7 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
)
node.delta_p = dp if power_mode else None
node.effective_gain = gain_target
node.effective_gain = gain_target
set_amplifier_voa(node, power_target, power_mode)
if isinstance(next_node, Roadm) or isinstance(next_node, Transceiver):
break
@@ -484,10 +506,13 @@ def add_fiber_padding(network, fibers, padding):
#add a padding att_in at the input of the 1st fiber:
#address the case when several fibers are spliced together
first_fiber = find_first_node(network, fiber)
if first_fiber.att_in is None:
first_fiber.att_in = padding - this_span_loss
else :
first_fiber.att_in = first_fiber.att_in + padding - this_span_loss
# in order to support no booster , fused might be placed
# just after a roadm: need to check that first_fiber is really a fiber
if isinstance(first_fiber,Fiber):
if first_fiber.att_in is None:
first_fiber.att_in = padding - this_span_loss
else:
first_fiber.att_in = first_fiber.att_in + padding - this_span_loss
def build_network(network, equipment, pref_ch_db, pref_total_db):
default_span_data = equipment['Span']['default']
@@ -510,6 +535,7 @@ def build_network(network, equipment, pref_ch_db, pref_total_db):
amplified_nodes = [n for n in network.nodes()
if isinstance(n, Fiber) or isinstance(n, Roadm)]
for node in amplified_nodes:
add_egress_amplifier(network, node)
@@ -522,4 +548,3 @@ def build_network(network, equipment, pref_ch_db, pref_total_db):
trx = [t for t in network.nodes() if isinstance(t, Transceiver)]
for t in trx:
set_egress_amplifier(network, t, equipment, pref_total_db)

View File

@@ -205,6 +205,36 @@
"longitude": 0
}
}
},
{
"uid": "Att_B",
"type": "Fused",
"params":{
"loss":16
},
"metadata": {
"location": {
"latitude": 2.0,
"longitude": 1.0,
"city": "Corlay",
"region": "RLD"
}
}
},
{
"uid": "Att_F",
"type": "Fused",
"params":{
"loss":16
},
"metadata": {
"location": {
"latitude": 2.0,
"longitude": 1.0,
"city": "Corlay",
"region": "RLD"
}
}
}
],
@@ -247,6 +277,10 @@
},
{
"from_node": "Edfa5",
"to_node": "Att_F"
},
{
"from_node": "Att_F",
"to_node": "trx F"
},
{
@@ -255,6 +289,10 @@
},
{
"from_node": "Edfa1",
"to_node": "Att_B"
},
{
"from_node": "Att_B",
"to_node": "trx B"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,16 @@
"p_max": 21,
"nf0": 5,
"allowed_for_design": true
}
},
{
"type_variety": "std_booster",
"type_def": "fixed_gain",
"gain_flatmax": 21,
"gain_min": 20,
"p_max": 21,
"nf0": 5,
"allowed_for_design": false
}
],
"Fiber":[{
"type_variety": "SSMF",
@@ -75,8 +84,8 @@
"target_pch_out_db": -20,
"add_drop_osnr": 38,
"restrictions": {
"preamp_variety_list":["low_gain_preamp", "high_gain_preamp"],
"booster_variety_list":["std_booster"]
"preamp_variety_list":[],
"booster_variety_list":[]
}
}],
"SI":[{

Binary file not shown.

View File

@@ -160,7 +160,11 @@
"uid": "roadm Lannion_CAS",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -175,7 +179,11 @@
"uid": "roadm Lorient_KMA",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -190,7 +198,11 @@
"uid": "roadm Vannes_KBE",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -205,7 +217,11 @@
"uid": "roadm Rennes_STA",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -220,7 +236,11 @@
"uid": "roadm Brest_KLA",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -235,7 +255,13 @@
"uid": "roadm a",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": [
"std_booster"
]
}
},
"metadata": {
"location": {
@@ -250,7 +276,13 @@
"uid": "roadm b",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [
"std_low_gain"
],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -265,7 +297,11 @@
"uid": "roadm c",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -280,7 +316,11 @@
"uid": "roadm d",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -295,7 +335,11 @@
"uid": "roadm e",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -310,7 +354,11 @@
"uid": "roadm f",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -325,7 +373,11 @@
"uid": "roadm g",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -340,7 +392,11 @@
"uid": "roadm h",
"type": "Roadm",
"params": {
"target_pch_out_db": -20
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
@@ -354,6 +410,9 @@
{
"uid": "west fused spans in Corlay",
"type": "Fused",
"params": {
"loss": 1
},
"metadata": {
"location": {
"latitude": 2.0,
@@ -366,6 +425,9 @@
{
"uid": "west fused spans in Loudeac",
"type": "Fused",
"params": {
"loss": 1
},
"metadata": {
"location": {
"latitude": 2.0,
@@ -378,6 +440,9 @@
{
"uid": "west fused spans in Morlaix",
"type": "Fused",
"params": {
"loss": 1
},
"metadata": {
"location": {
"latitude": 1.0,
@@ -390,6 +455,9 @@
{
"uid": "east fused spans in Corlay",
"type": "Fused",
"params": {
"loss": 1
},
"metadata": {
"location": {
"latitude": 2.0,
@@ -402,6 +470,9 @@
{
"uid": "east fused spans in Loudeac",
"type": "Fused",
"params": {
"loss": 1
},
"metadata": {
"location": {
"latitude": 2.0,
@@ -414,6 +485,9 @@
{
"uid": "east fused spans in Morlaix",
"type": "Fused",
"params": {
"loss": 1
},
"metadata": {
"location": {
"latitude": 1.0,
@@ -737,7 +811,7 @@
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 50.0,
"length": 10.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
@@ -1199,10 +1273,10 @@
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 50.0,
"length": 10.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"att_in": 8.0,
"con_in": 0,
"con_out": 0
},
@@ -1499,12 +1573,27 @@
}
}
},
{
"uid": "east edfa in c to d",
"type": "Fused",
"params": {
"loss": 0
},
"metadata": {
"location": {
"latitude": 6.0,
"longitude": 1.0,
"city": "c",
"region": ""
}
}
},
{
"uid": "Edfa0_roadm Lorient_KMA",
"type": "Edfa",
"type_variety": "test_fixed_gain",
"operational": {
"gain_target": 20,
"gain_target": 20.0,
"delta_p": null,
"tilt_target": 0,
"out_voa": 0
@@ -1523,7 +1612,7 @@
"type": "Edfa",
"type_variety": "test_fixed_gain",
"operational": {
"gain_target": 20,
"gain_target": 20.0,
"delta_p": null,
"tilt_target": 0,
"out_voa": 0
@@ -1654,7 +1743,7 @@
{
"uid": "Edfa0_roadm a",
"type": "Edfa",
"type_variety": "test_fixed_gain",
"type_variety": "std_booster",
"operational": {
"gain_target": 20,
"delta_p": null,
@@ -1673,7 +1762,7 @@
{
"uid": "Edfa1_roadm a",
"type": "Edfa",
"type_variety": "test_fixed_gain",
"type_variety": "std_booster",
"operational": {
"gain_target": 20,
"delta_p": null,
@@ -1732,7 +1821,7 @@
"type": "Edfa",
"type_variety": "test_fixed_gain",
"operational": {
"gain_target": 20,
"gain_target": 22,
"delta_p": null,
"tilt_target": 0,
"out_voa": 0
@@ -1751,26 +1840,7 @@
"type": "Edfa",
"type_variety": "test_fixed_gain",
"operational": {
"gain_target": 20,
"delta_p": null,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 6.0,
"longitude": 1.75,
"city": "c",
"region": ""
}
}
},
{
"uid": "Edfa2_roadm c",
"type": "Edfa",
"type_variety": "test_fixed_gain",
"operational": {
"gain_target": 20,
"gain_target": 22,
"delta_p": null,
"tilt_target": 0,
"out_voa": 0
@@ -2186,9 +2256,9 @@
{
"uid": "Edfa0_fiber (c → d)-",
"type": "Edfa",
"type_variety": "std_low_gain",
"type_variety": "test_fixed_gain",
"operational": {
"gain_target": 10.0,
"gain_target": 22.0,
"delta_p": null,
"tilt_target": 0,
"out_voa": 0
@@ -2723,6 +2793,10 @@
"from_node": "roadm Brest_KLA",
"to_node": "Edfa0_roadm Brest_KLA"
},
{
"from_node": "roadm c",
"to_node": "east edfa in c to d"
},
{
"from_node": "roadm a",
"to_node": "trx a"
@@ -2759,10 +2833,6 @@
"from_node": "roadm c",
"to_node": "Edfa1_roadm c"
},
{
"from_node": "roadm c",
"to_node": "Edfa2_roadm c"
},
{
"from_node": "roadm d",
"to_node": "trx d"
@@ -3051,6 +3121,10 @@
"from_node": "west edfa in Lannion_CAS to Morlaix",
"to_node": "roadm Lannion_CAS"
},
{
"from_node": "east edfa in c to d",
"to_node": "fiber (c → d)-"
},
{
"from_node": "Edfa0_roadm Lorient_KMA",
"to_node": "fiber (Lorient_KMA → Loudeac)-F054"
@@ -3105,10 +3179,6 @@
},
{
"from_node": "Edfa1_roadm c",
"to_node": "fiber (c → d)-"
},
{
"from_node": "Edfa2_roadm c",
"to_node": "fiber (c → f)-"
},
{
@@ -3284,4 +3354,4 @@
"to_node": "roadm h"
}
]
}
}

View File

@@ -218,6 +218,14 @@
},
{
"uid": "roadm a",
"params": {
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": [
"std_booster"
]
}
},
"metadata": {
"location": {
"city": "a",
@@ -230,6 +238,14 @@
},
{
"uid": "roadm b",
"params": {
"restrictions": {
"preamp_variety_list": [
"std_low_gain"
],
"booster_variety_list": []
}
},
"metadata": {
"location": {
"city": "b",
@@ -647,7 +663,7 @@
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"length": 50.0,
"length": 10.0,
"length_units": "km",
"loss_coef": 0.2,
"con_in": null,
@@ -1025,7 +1041,7 @@
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"length": 50.0,
"length": 10.0,
"length_units": "km",
"loss_coef": 0.2,
"con_in": null,
@@ -1291,6 +1307,21 @@
"tilt_target": 0,
"out_voa": null
}
},
{
"uid": "east edfa in c to d",
"metadata": {
"location": {
"city": "c",
"region": "",
"latitude": 6.0,
"longitude": 1.0
}
},
"type": "Fused",
"params": {
"loss": 0
}
}
],
"connections": [
@@ -1536,6 +1567,10 @@
},
{
"from_node": "roadm c",
"to_node": "east edfa in c to d"
},
{
"from_node": "east edfa in c to d",
"to_node": "fiber (c → d)-"
},
{
@@ -1743,4 +1778,4 @@
"to_node": "trx h"
}
]
}
}

View File

@@ -77,7 +77,22 @@
"longitude": 0
}
}
},
},
{
"uid": "Att_B",
"type": "Fused",
"params":{
"loss":16
},
"metadata": {
"location": {
"latitude": 2.0,
"longitude": 1.0,
"city": "Corlay",
"region": "RLD"
}
}
},
{
"uid": "Site_B",
"type": "Transceiver",
@@ -110,6 +125,10 @@
},
{
"from_node": "Edfa2",
"to_node": "Att_B"
},
{
"from_node": "Att_B",
"to_node": "Site_B"
}

View File

@@ -18,6 +18,8 @@ from numpy import mean
#network_file_name = 'tests/test_network.json'
network_file_name = Path(__file__).parent.parent / 'tests/LinkforTest.json'
#TODO: note that this json entries has a weird topology since EDfa1 has a possible branch on a receiver B
# this might not pass future tests/ code updates
#network_file_name = Path(__file__).parent.parent / 'examples/edfa_example_network.json'
eqpt_library_name = Path(__file__).parent.parent / 'tests/data/eqpt_config.json'

View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: Esther Le Rouzic
# @Date: 2019-05-22
"""
@author: esther.lerouzic
checks that fused placed in amp type is correctly converted to a fused element instead of an edfa
and that no additional amp is added.
checks that restrictions in roadms are correctly applied during autodesign
"""
from pathlib import Path
import pytest
from gnpy.core.utils import lin2db, load_json
from gnpy.core.elements import Fused, Roadm, Edfa
from gnpy.core.equipment import load_equipment, Amp, automatic_nch
from gnpy.core.network import network_from_json, build_network
TEST_DIR = Path(__file__).parent
EQPT_LIBRARY_NAME = TEST_DIR / 'data/eqpt_config.json'
NETWORK_FILE_NAME = TEST_DIR / 'data/testTopology_expected.json'
# adding tests to check the roadm restrictions
# mark node_uid amps as fused for testing purpose
@pytest.mark.parametrize("node_uid", ['east edfa in Lannion_CAS to Stbrieuc'])
def test_no_amp_feature(node_uid):
''' Check that booster is not placed on a roadm if fused is specified
test_parser covers partly this behaviour. This test should guaranty that the
feature is preserved even if convert is changed
'''
equipment = load_equipment(EQPT_LIBRARY_NAME)
json_network = load_json(NETWORK_FILE_NAME)
for elem in json_network['elements']:
if elem['uid'] == node_uid:
#replace edfa node by a fused node in the topology
elem['type'] = 'Fused'
elem.pop('type_variety')
elem.pop('operational')
elem['params'] = {'loss': 0}
next_node_uid = next(conn['to_node'] for conn in json_network['connections'] \
if conn['from_node'] == node_uid)
previous_node_uid = next(conn['from_node'] for conn in json_network['connections'] \
if conn['to_node'] == node_uid)
network = network_from_json(json_network, equipment)
# Build the network once using the default power defined in SI in eqpt config
# power density : db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
# spacing, f_min and f_max
p_db = equipment['SI']['default'].power_dbm
p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,\
equipment['SI']['default'].f_max, equipment['SI']['default'].spacing))
build_network(network, equipment, p_db, p_total_db)
node = next(nd for nd in network.nodes() if nd.uid == node_uid)
next_node = next(network.successors(node))
previous_node = next(network.predecessors(node))
if not isinstance(node, Fused):
raise AssertionError()
if not node.params.loss == 0.0:
raise AssertionError()
if not next_node_uid == next_node.uid:
raise AssertionError()
if not previous_node_uid == previous_node.uid:
raise AssertionError()
@pytest.fixture()
def equipment():
"""init transceiver class to access snr and osnr calculations"""
equipment = load_equipment(EQPT_LIBRARY_NAME)
# define some booster and preamps
restrictions_list = [
{
'type_variety': 'booster_medium_gain',
'type_def': 'variable_gain',
'gain_flatmax': 25,
'gain_min': 15,
'p_max': 21,
'nf_min': 5.8,
'nf_max': 10,
'out_voa_auto': False,
'allowed_for_design': False
},
{
'type_variety': 'preamp_medium_gain',
'type_def': 'variable_gain',
'gain_flatmax': 26,
'gain_min': 15,
'p_max': 23,
'nf_min': 6,
'nf_max': 10,
'out_voa_auto': False,
'allowed_for_design': False
},
{
'type_variety': 'preamp_high_gain',
'type_def': 'variable_gain',
'gain_flatmax': 35,
'gain_min': 25,
'p_max': 21,
'nf_min': 5.5,
'nf_max': 7,
'out_voa_auto': False,
'allowed_for_design': False
},
{
'type_variety': 'preamp_low_gain',
'type_def': 'variable_gain',
'gain_flatmax': 16,
'gain_min': 8,
'p_max': 23,
'nf_min': 6.5,
'nf_max': 11,
'out_voa_auto': False,
'allowed_for_design': False
}]
# add them to the library
for entry in restrictions_list:
equipment['Edfa'][entry['type_variety']] = Amp.from_json(EQPT_LIBRARY_NAME, **entry)
return equipment
@pytest.mark.parametrize("restrictions", [
{
'preamp_variety_list':[],
'booster_variety_list':[]
},
{
'preamp_variety_list':[],
'booster_variety_list':['booster_medium_gain']
},
{
'preamp_variety_list':['preamp_medium_gain', 'preamp_high_gain', 'preamp_low_gain'],
'booster_variety_list':[]
}])
def test_restrictions(restrictions, equipment):
''' test that restriction is correctly applied if provided in eqpt_config and if no Edfa type
were provided in the network json
'''
# add restrictions
equipment['Roadm']['default'].restrictions = restrictions
# build network
json_network = load_json(NETWORK_FILE_NAME)
network = network_from_json(json_network, equipment)
amp_nodes_nobuild_uid = [nd.uid for nd in network.nodes() \
if isinstance(nd, Edfa) and isinstance(next(network.predecessors(nd)), Roadm)]
preamp_nodes_nobuild_uid = [nd.uid for nd in network.nodes() \
if isinstance(nd, Edfa) and isinstance(next(network.successors(nd)), Roadm)]
amp_nodes_nobuild = {nd.uid : nd for nd in network.nodes() \
if isinstance(nd, Edfa) and isinstance(next(network.predecessors(nd)), Roadm)}
preamp_nodes_nobuild = {nd.uid : nd for nd in network.nodes() \
if isinstance(nd, Edfa) and isinstance(next(network.successors(nd)), Roadm)}
# roadm dict with restrictions before build
roadms = {nd.uid: nd for nd in network.nodes() if isinstance(nd, Roadm)}
# Build the network once using the default power defined in SI in eqpt config
# power density : db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
# spacing, f_min and f_max
p_db = equipment['SI']['default'].power_dbm
p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,\
equipment['SI']['default'].f_max, equipment['SI']['default'].spacing))
build_network(network, equipment, p_db, p_total_db)
amp_nodes = [nd for nd in network.nodes() \
if isinstance(nd, Edfa) and isinstance(next(network.predecessors(nd)), Roadm)\
and next(network.predecessors(nd)).restrictions['booster_variety_list']]
preamp_nodes = [nd for nd in network.nodes() \
if isinstance(nd, Edfa) and isinstance(next(network.successors(nd)), Roadm)\
and next(network.successors(nd)).restrictions['preamp_variety_list']]
# check that previously existing amp are not changed
for amp in amp_nodes:
if amp.uid in amp_nodes_nobuild_uid:
print(amp.uid, amp.params.type_variety)
if not amp.params.type_variety == amp_nodes_nobuild[amp.uid].params.type_variety:
raise AssertionError()
for amp in preamp_nodes:
if amp.uid in preamp_nodes_nobuild_uid:
if not amp.params.type_variety == preamp_nodes_nobuild[amp.uid].params.type_variety:
raise AssertionError()
# check that restrictions are correctly applied
for amp in amp_nodes:
if amp.uid not in amp_nodes_nobuild_uid:
# and if roadm had no restrictions before build:
if restrictions['booster_variety_list'] and \
not roadms[next(network.predecessors(amp)).uid]\
.restrictions['booster_variety_list']:
if not amp.params.type_variety in restrictions['booster_variety_list']:
raise AssertionError()
for amp in preamp_nodes:
if amp.uid not in preamp_nodes_nobuild_uid:
if restrictions['preamp_variety_list'] and\
not roadms[next(network.successors(amp)).uid].restrictions['preamp_variety_list']:
if not amp.params.type_variety in restrictions['preamp_variety_list']:
raise AssertionError()