Skip to content

Commit

Permalink
Restrictions on auto-adding amplifiers into ROADMs
Browse files Browse the repository at this point in the history
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.

In case a per-degree exception is needed, add the `Fused` node to a
specific degree. When the `Fused` element is present, the restrictions
no longer apply, and amplifiers can be placed manually.

Signed-off-by: Esther Le Rouzic <esther.lerouzic@orange.com>
Co-authored-by: Jan Kundrát <jan.kundrat@telecominfraproject.com>
  • Loading branch information
EstherLerouzic and jktjkt committed May 31, 2019
1 parent 166d8a3 commit ff22e5f
Show file tree
Hide file tree
Showing 14 changed files with 995 additions and 189 deletions.
8 changes: 6 additions & 2 deletions Excel_userguide.rst
Expand Up @@ -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

Expand All @@ -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**


Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions examples/eqpt_config.json
Expand Up @@ -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":[{
Expand Down
Binary file modified examples/meshTopologyExampleV2.xls
Binary file not shown.
31 changes: 24 additions & 7 deletions gnpy/core/convert.py
Expand Up @@ -31,6 +31,7 @@
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))
Expand All @@ -54,7 +55,9 @@ def update_attr(self, kwargs):
'region': '',
'latitude': 0,
'longitude': 0,
'node_type': 'ILA'
'node_type': 'ILA',
'booster_restriction' : '',
'preamp_restriction' : ''
}

class Link(object):
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -414,7 +429,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',
Expand Down Expand Up @@ -571,7 +588,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
Expand Down
13 changes: 10 additions & 3 deletions gnpy/core/elements.py
Expand Up @@ -117,7 +117,7 @@ def __call__(self, spectral_info):
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):
Expand All @@ -126,15 +126,19 @@ def __init__(self, *args, params, **kwargs):
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):
Expand Down Expand Up @@ -186,6 +190,9 @@ def __init__(self, *args, params=None, **kwargs):
def to_json(self):
return {'uid' : self.uid,
'type' : type(self).__name__,
'params' :{
'loss': self.loss
},
'metadata' : {
'location': self.metadata['location']._asdict()
}
Expand Down
88 changes: 53 additions & 35 deletions gnpy/core/equipment.py
Expand Up @@ -27,28 +27,28 @@

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):
default_values =\
{
"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):
Expand All @@ -69,16 +69,20 @@ class Span(common):
'con_in': 0,
'con_out': 0
}

def __init__(self, **kwargs):
self.update_attr(self.default_values, kwargs, 'Span')

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')
Expand Down Expand Up @@ -134,7 +138,7 @@ def from_json(cls, filename, **kwargs):
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

Expand Down Expand Up @@ -180,7 +184,7 @@ def from_json(cls, filename, **kwargs):
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})


Expand Down Expand Up @@ -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
Expand All @@ -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']

Expand All @@ -298,16 +302,16 @@ 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

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):
Expand All @@ -333,18 +337,31 @@ 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:
try:
if equipment['Edfa'][amp_name]:
pass
except KeyError:
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
Expand All @@ -359,11 +376,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

0 comments on commit ff22e5f

Please sign in to comment.