mirror of
https://github.com/Telecominfraproject/oopt-gnpy.git
synced 2025-10-30 17:47:50 +00:00
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com> Change-Id: I24feed756586a104e829275244f0868a272e5f6b
732 lines
32 KiB
Python
732 lines
32 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
# SPDX-License-Identifier: BSD-3-Clause
|
||
# gnpy.core.parameters: parameters to configure standard network elements
|
||
# Copyright (C) 2025 Telecom Infra Project and GNPy contributors
|
||
# see AUTHORS.rst for a list of contributors
|
||
|
||
"""
|
||
gnpy.core.parameters
|
||
====================
|
||
|
||
This module contains all parameters to configure standard network elements.
|
||
"""
|
||
from collections import namedtuple
|
||
from copy import deepcopy
|
||
from dataclasses import dataclass
|
||
from scipy.constants import c, pi
|
||
from numpy import asarray, array, exp, sqrt, log, outer, ones, squeeze, append, flip, linspace, full
|
||
|
||
from gnpy.core.utils import convert_length
|
||
from gnpy.core.exceptions import ParametersError
|
||
|
||
|
||
class Parameters:
|
||
def asdict(self):
|
||
class_dict = self.__class__.__dict__
|
||
instance_dict = self.__dict__
|
||
new_dict = {}
|
||
for key in class_dict:
|
||
if isinstance(class_dict[key], property):
|
||
new_dict[key] = instance_dict['_' + key]
|
||
return new_dict
|
||
|
||
|
||
class PumpParams(Parameters):
|
||
def __init__(self, power, frequency, propagation_direction):
|
||
self.power = power
|
||
self.frequency = frequency
|
||
self.propagation_direction = propagation_direction.lower()
|
||
|
||
|
||
class RamanParams(Parameters):
|
||
def __init__(self, flag=False, method='perturbative', order=2, result_spatial_resolution=10e3,
|
||
solver_spatial_resolution=10e3):
|
||
"""Simulation parameters used within the Raman Solver
|
||
|
||
:params flag: boolean for enabling/disable the evaluation of the Raman power profile in frequency and position
|
||
:params method: Raman solver method
|
||
:params order: solution order for perturbative method
|
||
:params result_spatial_resolution: spatial resolution of the evaluated Raman power profile
|
||
:params solver_spatial_resolution: spatial step for the iterative solution of the first order ode
|
||
"""
|
||
self.flag = flag
|
||
self.method = method
|
||
self.order = order
|
||
self.result_spatial_resolution = result_spatial_resolution # [m]
|
||
self.solver_spatial_resolution = solver_spatial_resolution # [m]
|
||
|
||
def to_json(self):
|
||
return {"flag": self.flag,
|
||
"method": self.method,
|
||
"order": self.order,
|
||
"result_spatial_resolution": self.result_spatial_resolution,
|
||
"solver_spatial_resolution": self.solver_spatial_resolution}
|
||
|
||
|
||
class NLIParams(Parameters):
|
||
def __init__(self, method='gn_model_analytic', dispersion_tolerance=4, phase_shift_tolerance=0.1,
|
||
computed_channels=None, computed_number_of_channels=None):
|
||
"""Simulation parameters used within the Nli Solver
|
||
|
||
:params method: formula for NLI calculation
|
||
:params dispersion_tolerance: tuning parameter for ggn model solution
|
||
:params phase_shift_tolerance: tuning parameter for ggn model solution
|
||
:params computed_channels: the NLI is evaluated for these channels and extrapolated for the others
|
||
:params computed_number_of_channels: the NLI is evaluated for this number of channels equally distributed
|
||
in the spectrum and extrapolated for the others
|
||
"""
|
||
self.method = method.lower()
|
||
self.dispersion_tolerance = dispersion_tolerance
|
||
self.phase_shift_tolerance = phase_shift_tolerance
|
||
self.computed_channels = computed_channels
|
||
self.computed_number_of_channels = computed_number_of_channels
|
||
|
||
def to_json(self):
|
||
return {"method": self.method,
|
||
"dispersion_tolerance": self.dispersion_tolerance,
|
||
"phase_shift_tolerance": self.phase_shift_tolerance,
|
||
"computed_channels": self.computed_channels,
|
||
"computed_number_of_channels": self.computed_number_of_channels}
|
||
|
||
|
||
class SimParams(Parameters):
|
||
_shared_dict = {'nli_params': NLIParams(), 'raman_params': RamanParams()}
|
||
|
||
@classmethod
|
||
def set_params(cls, sim_params):
|
||
cls._shared_dict['nli_params'] = NLIParams(**sim_params.get('nli_params', {}))
|
||
cls._shared_dict['raman_params'] = RamanParams(**sim_params.get('raman_params', {}))
|
||
|
||
@property
|
||
def nli_params(self):
|
||
return self._shared_dict['nli_params']
|
||
|
||
@property
|
||
def raman_params(self):
|
||
return self._shared_dict['raman_params']
|
||
|
||
|
||
class RoadmParams(Parameters):
|
||
def __init__(self, **kwargs):
|
||
self.target_pch_out_db = kwargs.get('target_pch_out_db')
|
||
self.target_psd_out_mWperGHz = kwargs.get('target_psd_out_mWperGHz')
|
||
self.target_out_mWperSlotWidth = kwargs.get('target_out_mWperSlotWidth')
|
||
equalisation_type = ['target_pch_out_db', 'target_psd_out_mWperGHz', 'target_out_mWperSlotWidth']
|
||
temp = [kwargs.get(k) is not None for k in equalisation_type]
|
||
if sum(temp) > 1:
|
||
raise ParametersError('ROADM config contains more than one equalisation type.'
|
||
+ 'Please choose only one', kwargs)
|
||
self.per_degree_pch_out_db = kwargs.get('per_degree_pch_out_db', {})
|
||
self.per_degree_pch_psd = kwargs.get('per_degree_psd_out_mWperGHz', {})
|
||
self.per_degree_pch_psw = kwargs.get('per_degree_psd_out_mWperSlotWidth', {})
|
||
try:
|
||
self.add_drop_osnr = kwargs['add_drop_osnr']
|
||
self.pmd = kwargs['pmd']
|
||
self.pdl = kwargs['pdl']
|
||
self.restrictions = kwargs['restrictions']
|
||
self.roadm_path_impairments = self.get_roadm_path_impairments(kwargs['roadm-path-impairments'])
|
||
except KeyError as e:
|
||
raise ParametersError(f'ROADM configurations must include {e}. Configuration: {kwargs}')
|
||
self.per_degree_impairments = kwargs.get('per_degree_impairments', [])
|
||
self.design_bands = kwargs.get('design_bands', [])
|
||
self.per_degree_design_bands = kwargs.get('per_degree_design_bands', {})
|
||
|
||
def get_roadm_path_impairments(self, path_impairments_list):
|
||
"""Get the ROADM list of profiles for impairments definition
|
||
|
||
transform the ietf model into gnpy internal model: add a path-type in the attributes
|
||
"""
|
||
if not path_impairments_list:
|
||
return {}
|
||
authorized_path_types = {
|
||
'roadm-express-path': 'express',
|
||
'roadm-add-path': 'add',
|
||
'roadm-drop-path': 'drop',
|
||
}
|
||
roadm_path_impairments = {}
|
||
for path_impairment in path_impairments_list:
|
||
index = path_impairment['roadm-path-impairments-id']
|
||
path_type = next(key for key in path_impairment if key in authorized_path_types.keys())
|
||
impairment_dict = {'path-type': authorized_path_types[path_type], 'impairment': path_impairment[path_type]}
|
||
roadm_path_impairments[index] = RoadmImpairment(impairment_dict)
|
||
return roadm_path_impairments
|
||
|
||
|
||
class RoadmPath:
|
||
def __init__(self, from_degree, to_degree, path_type, impairment_id=None, impairment=None):
|
||
"""Records roadm internal paths, types and impairment
|
||
|
||
path_type must be in "express", "add", "drop"
|
||
impairment_id must be one of the id detailed in equipement
|
||
"""
|
||
self.from_degree = from_degree
|
||
self.to_degree = to_degree
|
||
self.path_type = path_type
|
||
self.impairment_id = impairment_id
|
||
self.impairment = impairment
|
||
|
||
|
||
class RoadmImpairment:
|
||
"""Generic definition of impairments for express, add and drop"""
|
||
default_values = {
|
||
'roadm-pmd': None,
|
||
'roadm-cd': None,
|
||
'roadm-pdl': None,
|
||
'roadm-inband-crosstalk': None,
|
||
'roadm-maxloss': 0,
|
||
'roadm-osnr': None,
|
||
'roadm-pmax': None,
|
||
'roadm-noise-figure': None,
|
||
'minloss': None,
|
||
'typloss': None,
|
||
'pmin': None,
|
||
'ptyp': None
|
||
}
|
||
|
||
def __init__(self, params):
|
||
self.path_type = params.get('path-type')
|
||
self.impairments = params['impairment']
|
||
|
||
|
||
class FusedParams(Parameters):
|
||
def __init__(self, **kwargs):
|
||
self.loss = kwargs['loss'] if 'loss' in kwargs else 1
|
||
|
||
|
||
DEFAULT_RAMAN_COEFFICIENT = {
|
||
# SSMF Raman coefficient profile in terms of mode intensity (g0 * A_ff_overlap)
|
||
'gamma_raman': array(
|
||
[0.0, 8.524419934705497e-16, 2.643567866245371e-15, 4.410548410941305e-15, 6.153422961291078e-15,
|
||
7.484924703044943e-15, 8.452060808349209e-15, 9.101549322698156e-15, 9.57837595158966e-15,
|
||
1.0008642675474562e-14, 1.0865773569905647e-14, 1.1300776305865833e-14, 1.2143238647099625e-14,
|
||
1.3231065750676068e-14, 1.4624900971525384e-14, 1.6013330554840492e-14, 1.7458119359310242e-14,
|
||
1.9320241330434762e-14, 2.1720395392873534e-14, 2.4137337406734775e-14, 2.628163218460466e-14,
|
||
2.8041019963285974e-14, 2.9723155447089933e-14, 3.129353531005888e-14, 3.251796163324624e-14,
|
||
3.3198839487612773e-14, 3.329527690685666e-14, 3.313155691238456e-14, 3.289013852154548e-14,
|
||
3.2458917188506916e-14, 3.060684277937575e-14, 3.2660349473783173e-14, 2.957419109657689e-14,
|
||
2.518894321396672e-14, 1.734560485857344e-14, 9.902860761605233e-15, 7.219176385099358e-15,
|
||
6.079565990401311e-15, 5.828373065963427e-15, 7.20580801091692e-15, 7.561924351387493e-15,
|
||
7.621152352332206e-15, 6.8859886780643254e-15, 5.629181047471162e-15, 3.679727598966185e-15,
|
||
2.7555869742500355e-15, 2.4810133942597675e-15, 2.2160080532403624e-15, 2.1440626024765557e-15,
|
||
2.33873070799544e-15, 2.557317929858713e-15, 3.039839048226572e-15, 4.8337165515610065e-15,
|
||
5.4647431818257436e-15, 5.229187813711269e-15, 4.510768525811313e-15, 3.3213473130607794e-15,
|
||
2.2602577027996455e-15, 1.969576495866441e-15, 1.5179853954188527e-15, 1.2953988551200156e-15,
|
||
1.1304672156251838e-15, 9.10004390675213e-16, 8.432919922183503e-16, 7.849224069008326e-16,
|
||
7.827568196032024e-16, 9.000514440646232e-16, 1.3025926460013665e-15, 1.5444108938497558e-15,
|
||
1.8795594063060786e-15, 1.7796130169921014e-15, 1.5938159865046653e-15, 1.1585522355108287e-15,
|
||
8.507044444633358e-16, 7.625404663756823e-16, 8.14510750925789e-16, 9.047944693473188e-16,
|
||
9.636431901702084e-16, 9.298633899602105e-16, 8.349739503637023e-16, 7.482901278066085e-16,
|
||
6.240794767134268e-16, 5.00652535687506e-16, 3.553373263685851e-16, 2.0344217706119682e-16,
|
||
1.4267522642294203e-16, 8.980016576743517e-17, 2.9829068181832594e-17, 1.4861959129014824e-17,
|
||
7.404482113326137e-18]
|
||
), # m/W
|
||
# SSMF Raman coefficient profile
|
||
'g0': array(
|
||
[0.00000000e+00, 1.12351610e-05, 3.47838074e-05, 5.79356636e-05, 8.06921680e-05, 9.79845709e-05, 1.10454361e-04,
|
||
1.18735302e-04, 1.24736889e-04, 1.30110053e-04, 1.41001273e-04, 1.46383247e-04, 1.57011792e-04, 1.70765865e-04,
|
||
1.88408911e-04, 2.05914127e-04, 2.24074028e-04, 2.47508283e-04, 2.77729174e-04, 3.08044243e-04, 3.34764439e-04,
|
||
3.56481704e-04, 3.77127256e-04, 3.96269124e-04, 4.10955175e-04, 4.18718761e-04, 4.19511263e-04, 4.17025384e-04,
|
||
4.13565369e-04, 4.07726048e-04, 3.83671291e-04, 4.08564283e-04, 3.69571936e-04, 3.14442090e-04, 2.16074535e-04,
|
||
1.23097823e-04, 8.95457457e-05, 7.52470400e-05, 7.19806145e-05, 8.87961158e-05, 9.30812065e-05, 9.37058268e-05,
|
||
8.45719619e-05, 6.90585286e-05, 4.50407159e-05, 3.36521245e-05, 3.02292475e-05, 2.69376939e-05, 2.60020897e-05,
|
||
2.82958958e-05, 3.08667558e-05, 3.66024657e-05, 5.80610307e-05, 6.54797937e-05, 6.25022715e-05, 5.37806442e-05,
|
||
3.94996621e-05, 2.68120644e-05, 2.33038554e-05, 1.79140757e-05, 1.52472424e-05, 1.32707565e-05, 1.06541760e-05,
|
||
9.84649374e-06, 9.13999627e-06, 9.08971012e-06, 1.04227525e-05, 1.50419271e-05, 1.77838232e-05, 2.15810815e-05,
|
||
2.03744008e-05, 1.81939341e-05, 1.31862121e-05, 9.65352116e-06, 8.62698322e-06, 9.18688016e-06, 1.01737784e-05,
|
||
1.08017817e-05, 1.03903588e-05, 9.30040333e-06, 8.30809173e-06, 6.90650401e-06, 5.52238029e-06, 3.90648708e-06,
|
||
2.22908227e-06, 1.55796177e-06, 9.77218716e-07, 3.23477236e-07, 1.60602454e-07, 7.97306386e-08]
|
||
), # [1 / (W m)]
|
||
|
||
# Note the non-uniform spacing of this range; this is required for properly capturing the Raman peak shape.
|
||
'frequency_offset': array([
|
||
0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4., 4.5, 5., 5.5, 6., 6.5, 7., 7.5, 8., 8.5, 9., 9.5, 10., 10.5, 11., 11.5,
|
||
12., 12.5, 12.75, 13., 13.25, 13.5, 14., 14.5, 14.75, 15., 15.5, 16., 16.5, 17., 17.5, 18., 18.25, 18.5, 18.75,
|
||
19., 19.5, 20., 20.5, 21., 21.5, 22., 22.5, 23., 23.5, 24., 24.5, 25., 25.5, 26., 26.5, 27., 27.5, 28., 28.5,
|
||
29., 29.5, 30., 30.5, 31., 31.5, 32., 32.5, 33., 33.5, 34., 34.5, 35., 35.5, 36., 36.5, 37., 37.5, 38., 38.5,
|
||
39., 39.5, 40., 40.5, 41., 41.5, 42.]) * 1e12, # [Hz]
|
||
|
||
# Raman profile reference frequency
|
||
'reference_frequency': 206.184634112792e12, # [Hz] (1454 nm)
|
||
|
||
# Raman profile reference effective area
|
||
'reference_effective_area': 75.74659443542413e-12 # [m^2] (@1454 nm)
|
||
}
|
||
|
||
|
||
class RamanGainCoefficient(namedtuple('RamanGainCoefficient', 'normalized_gamma_raman frequency_offset')):
|
||
""" Raman Gain Coefficient Parameters
|
||
|
||
Based on:
|
||
Andrea D’Amico, Bruno Correia, Elliot London, Emanuele Virgillito, Giacomo Borraccini, Antonio Napoli,
|
||
and Vittorio Curri, "Scalable and Disaggregated GGN Approximation Applied to a C+L+S Optical Network,"
|
||
J. Lightwave Technol. 40, 3499-3511 (2022)
|
||
Section III.D
|
||
"""
|
||
|
||
|
||
class FiberParams(Parameters):
|
||
def __init__(self, **kwargs):
|
||
try:
|
||
self._length = convert_length(kwargs['length'], kwargs['length_units'])
|
||
# fixed attenuator for padding
|
||
self._att_in = kwargs.get('att_in', 0)
|
||
# if not defined in the network json connector loss in/out
|
||
# the None value will be updated in network.py[build_network]
|
||
# with default values from eqpt_config.json[Spans]
|
||
self._con_in = kwargs.get('con_in')
|
||
self._con_out = kwargs.get('con_out')
|
||
|
||
# Reference frequency (unique for all parameters: beta2, beta3, gamma, effective_area)
|
||
if 'ref_wavelength' in kwargs:
|
||
self._ref_wavelength = kwargs['ref_wavelength']
|
||
self._ref_frequency = c / self._ref_wavelength
|
||
elif 'ref_frequency' in kwargs:
|
||
self._ref_frequency = kwargs['ref_frequency']
|
||
self._ref_wavelength = c / self._ref_frequency
|
||
else:
|
||
self._ref_wavelength = 1550e-9 # conventional central C band wavelength [m]
|
||
self._ref_frequency = c / self._ref_wavelength
|
||
|
||
# Chromatic Dispersion
|
||
if 'dispersion_per_frequency' in kwargs:
|
||
# Frequency-dependent dispersion
|
||
self._dispersion = asarray(kwargs['dispersion_per_frequency']['value']) # s/m/m
|
||
self._f_dispersion_ref = asarray(kwargs['dispersion_per_frequency']['frequency']) # Hz
|
||
self._dispersion_slope = None
|
||
elif 'dispersion' in kwargs:
|
||
# Single value dispersion
|
||
self._dispersion = asarray(kwargs['dispersion']) # s/m/m
|
||
self._dispersion_slope = kwargs.get('dispersion_slope') # s/m/m/m
|
||
self._f_dispersion_ref = asarray(self._ref_frequency) # Hz
|
||
else:
|
||
# Default single value dispersion
|
||
self._dispersion = asarray(1.67e-05) # s/m/m
|
||
self._dispersion_slope = None
|
||
self._f_dispersion_ref = asarray(self.ref_frequency) # Hz
|
||
|
||
# Effective Area and Nonlinear Coefficient
|
||
self._effective_area = kwargs.get('effective_area') # m^2
|
||
self._n1 = 1.468
|
||
self._core_radius = 4.2e-6 # m
|
||
self._n2 = 2.6e-20 # m^2/W
|
||
if self._effective_area is not None:
|
||
default_gamma = 2 * pi * self._n2 / (self._ref_wavelength * self._effective_area)
|
||
self._gamma = kwargs.get('gamma', default_gamma) # 1/W/m
|
||
elif 'gamma' in kwargs:
|
||
self._gamma = kwargs['gamma'] # 1/W/m
|
||
self._effective_area = 2 * pi * self._n2 / (self._ref_wavelength * self._gamma) # m^2
|
||
else:
|
||
self._effective_area = 83e-12 # m^2
|
||
self._gamma = 2 * pi * self._n2 / (self._ref_wavelength * self._effective_area) # 1/W/m
|
||
self._contrast = 0.5 * (c / (2 * pi * self._ref_frequency * self._core_radius * self._n1) * exp(
|
||
pi * self._core_radius ** 2 / self._effective_area)) ** 2
|
||
|
||
# Raman Gain Coefficient
|
||
raman_coefficient = kwargs.get('raman_coefficient')
|
||
if raman_coefficient is None:
|
||
self._raman_reference_frequency = DEFAULT_RAMAN_COEFFICIENT['reference_frequency']
|
||
frequency_offset = asarray(DEFAULT_RAMAN_COEFFICIENT['frequency_offset'])
|
||
gamma_raman = asarray(DEFAULT_RAMAN_COEFFICIENT['gamma_raman'])
|
||
stokes_wave = self._raman_reference_frequency - frequency_offset
|
||
normalized_gamma_raman = gamma_raman / self._raman_reference_frequency # 1 / m / W / Hz
|
||
self._g0 = gamma_raman / self.effective_area_overlap(stokes_wave, self._raman_reference_frequency)
|
||
else:
|
||
self._raman_reference_frequency = raman_coefficient['reference_frequency']
|
||
frequency_offset = asarray(raman_coefficient['frequency_offset'])
|
||
stokes_wave = self._raman_reference_frequency - frequency_offset
|
||
self._g0 = asarray(raman_coefficient['g0'])
|
||
gamma_raman = self._g0 * self.effective_area_overlap(stokes_wave, self._raman_reference_frequency)
|
||
normalized_gamma_raman = gamma_raman / self._raman_reference_frequency # 1 / m / W / Hz
|
||
|
||
# Raman gain coefficient array of the frequency offset constructed such that positive frequency values
|
||
# represent a positive power transfer from higher frequency and vice versa
|
||
frequency_offset = append(-flip(frequency_offset[1:]), frequency_offset)
|
||
normalized_gamma_raman = append(- flip(normalized_gamma_raman[1:]), normalized_gamma_raman)
|
||
self._raman_coefficient = RamanGainCoefficient(normalized_gamma_raman, frequency_offset)
|
||
|
||
# Polarization Mode Dispersion
|
||
self._pmd_coef = kwargs['pmd_coef'] # s/sqrt(m)
|
||
self._pmd_coef_defined = kwargs.get('pmd_coef_defined', kwargs['pmd_coef'] is True)
|
||
|
||
# Loss Coefficient
|
||
if isinstance(kwargs['loss_coef'], dict):
|
||
self._loss_coef = asarray(kwargs['loss_coef']['value']) * 1e-3 # lineic loss dB/m
|
||
self._f_loss_ref = asarray(kwargs['loss_coef']['frequency']) # Hz
|
||
else:
|
||
self._loss_coef = asarray(kwargs['loss_coef']) * 1e-3 # lineic loss dB/m
|
||
self._f_loss_ref = asarray(self._ref_frequency) # Hz
|
||
# Lumped Losses
|
||
self._lumped_losses = kwargs['lumped_losses'] if 'lumped_losses' in kwargs else array([])
|
||
self._latency = self._length / (c / self._n1) # s
|
||
except KeyError as e:
|
||
raise ParametersError(f'Fiber configurations json must include {e}. Configuration: {kwargs}')
|
||
|
||
@property
|
||
def length(self):
|
||
return self._length
|
||
|
||
@length.setter
|
||
def length(self, length):
|
||
"""length must be in m"""
|
||
self._length = length
|
||
|
||
@property
|
||
def att_in(self):
|
||
return self._att_in
|
||
|
||
@att_in.setter
|
||
def att_in(self, att_in):
|
||
self._att_in = att_in
|
||
|
||
@property
|
||
def con_in(self):
|
||
return self._con_in
|
||
|
||
@con_in.setter
|
||
def con_in(self, con_in):
|
||
self._con_in = con_in
|
||
|
||
@property
|
||
def con_out(self):
|
||
return self._con_out
|
||
|
||
@property
|
||
def lumped_losses(self):
|
||
return self._lumped_losses
|
||
|
||
@con_out.setter
|
||
def con_out(self, con_out):
|
||
self._con_out = con_out
|
||
|
||
@property
|
||
def dispersion(self):
|
||
return self._dispersion
|
||
|
||
@property
|
||
def f_dispersion_ref(self):
|
||
return self._f_dispersion_ref
|
||
|
||
@property
|
||
def dispersion_slope(self):
|
||
return self._dispersion_slope
|
||
|
||
@property
|
||
def gamma(self):
|
||
return self._gamma
|
||
|
||
def effective_area_scaling(self, frequency):
|
||
V = 2 * pi * frequency / c * self._core_radius * self._n1 * sqrt(2 * self._contrast)
|
||
w = self._core_radius / sqrt(log(V))
|
||
return asarray(pi * w ** 2)
|
||
|
||
def effective_area_overlap(self, frequency_stokes_wave, frequency_pump):
|
||
effective_area_stokes_wave = self.effective_area_scaling(frequency_stokes_wave)
|
||
effective_area_pump = self.effective_area_scaling(frequency_pump)
|
||
return squeeze(outer(effective_area_stokes_wave, ones(effective_area_pump.size)) + outer(
|
||
ones(effective_area_stokes_wave.size), effective_area_pump)) / 2
|
||
|
||
def gamma_scaling(self, frequency):
|
||
return asarray(2 * pi * self._n2 * frequency / (c * self.effective_area_scaling(frequency)))
|
||
|
||
@property
|
||
def pmd_coef(self):
|
||
return self._pmd_coef
|
||
|
||
@property
|
||
def pmd_coef_defined(self):
|
||
return self._pmd_coef_defined
|
||
|
||
@property
|
||
def ref_wavelength(self):
|
||
return self._ref_wavelength
|
||
|
||
@property
|
||
def ref_frequency(self):
|
||
return self._ref_frequency
|
||
|
||
@property
|
||
def loss_coef(self):
|
||
return self._loss_coef
|
||
|
||
@property
|
||
def f_loss_ref(self):
|
||
return self._f_loss_ref
|
||
|
||
@property
|
||
def raman_coefficient(self):
|
||
return self._raman_coefficient
|
||
|
||
@property
|
||
def latency(self):
|
||
return self._latency
|
||
|
||
def asdict(self):
|
||
dictionary = super().asdict()
|
||
dictionary['loss_coef'] = self.loss_coef * 1e3
|
||
dictionary['length_units'] = 'm'
|
||
if len(self.lumped_losses) == 0:
|
||
dictionary.pop('lumped_losses')
|
||
if not self.raman_coefficient:
|
||
dictionary.pop('raman_coefficient')
|
||
else:
|
||
raman_frequency_offset = \
|
||
self.raman_coefficient.frequency_offset[self.raman_coefficient.frequency_offset >= 0]
|
||
dictionary['raman_coefficient'] = {'g0': self._g0.tolist(),
|
||
'frequency_offset': raman_frequency_offset.tolist(),
|
||
'reference_frequency': self._raman_reference_frequency}
|
||
return dictionary
|
||
|
||
|
||
class EdfaParams:
|
||
default_values = {
|
||
'f_min': None,
|
||
'f_max': None,
|
||
'multi_band': None,
|
||
'bands': None,
|
||
'type_variety': '',
|
||
'type_def': '',
|
||
'gain_flatmax': None,
|
||
'gain_min': None,
|
||
'p_max': None,
|
||
'nf_model': None,
|
||
'dual_stage_model': None,
|
||
'preamp_variety': None,
|
||
'booster_variety': None,
|
||
'nf_min': None,
|
||
'nf_max': None,
|
||
'nf_coef': None,
|
||
'nf0': None,
|
||
'nf_fit_coeff': None,
|
||
'nf_ripple': 0,
|
||
'dgt': None,
|
||
'gain_ripple': 0,
|
||
'tilt_ripple': 0,
|
||
'f_ripple_ref': None,
|
||
'out_voa_auto': False,
|
||
'allowed_for_design': False,
|
||
'raman': False,
|
||
'pmd': 0,
|
||
'pdl': 0,
|
||
'advance_configurations_from_json': None
|
||
}
|
||
|
||
def __init__(self, **params):
|
||
try:
|
||
self.type_variety = params['type_variety']
|
||
self.type_def = params['type_def']
|
||
|
||
# Bandwidth
|
||
self.f_min = params['f_min']
|
||
self.f_max = params['f_max']
|
||
self.bandwidth = self.f_max - self.f_min if self.f_max and self.f_min else None
|
||
self.f_cent = (self.f_max + self.f_min) / 2 if self.f_max and self.f_min else None
|
||
self.f_ripple_ref = params['f_ripple_ref']
|
||
self.bands = [{'f_min': params['f_min'],
|
||
'f_max': params['f_max']}]
|
||
|
||
# Gain
|
||
self.gain_flatmax = params['gain_flatmax']
|
||
self.gain_min = params['gain_min']
|
||
|
||
gain_ripple = params['gain_ripple']
|
||
if gain_ripple == 0:
|
||
self.gain_ripple = asarray([0, 0])
|
||
self.f_ripple_ref = asarray([self.f_min, self.f_max])
|
||
else:
|
||
self.gain_ripple = asarray(gain_ripple)
|
||
if self.f_ripple_ref is not None:
|
||
if (self.f_ripple_ref[0] != self.f_min) or (self.f_ripple_ref[-1] != self.f_max):
|
||
raise ParametersError("The reference ripple frequency maximum and minimum have to coincide "
|
||
"with the EDFA frequency maximum and minimum.")
|
||
elif self.gain_ripple.size != self.f_ripple_ref.size:
|
||
raise ParametersError("The reference ripple frequency and the gain ripple must have the same "
|
||
"size.")
|
||
else:
|
||
self.f_ripple_ref = linspace(self.f_min, self.f_max, self.gain_ripple.size)
|
||
|
||
tilt_ripple = params['tilt_ripple']
|
||
|
||
if tilt_ripple == 0:
|
||
self.tilt_ripple = full(self.gain_ripple.size, 0)
|
||
else:
|
||
self.tilt_ripple = asarray(tilt_ripple)
|
||
if self.tilt_ripple.size != self.gain_ripple.size:
|
||
raise ParametersError("The tilt ripple and the gain ripple must have the same size.")
|
||
|
||
# Power
|
||
self.p_max = params['p_max']
|
||
|
||
# Noise Figure
|
||
self.nf_model = params['nf_model']
|
||
self.nf_min = params['nf_min']
|
||
self.nf_max = params['nf_max']
|
||
self.nf_coef = params['nf_coef']
|
||
self.nf0 = params['nf0']
|
||
self.nf_fit_coeff = params['nf_fit_coeff']
|
||
|
||
nf_ripple = params['nf_ripple']
|
||
if nf_ripple == 0:
|
||
self.nf_ripple = full(self.gain_ripple.size, 0)
|
||
else:
|
||
self.nf_ripple = asarray(nf_ripple)
|
||
if self.nf_ripple.size != self.gain_ripple.size:
|
||
raise ParametersError(
|
||
"The noise figure ripple and the gain ripple must have the same size. %s, %s",
|
||
self.nf_ripple.size, self.gain_ripple.size)
|
||
|
||
# VOA
|
||
self.out_voa_auto = params['out_voa_auto']
|
||
|
||
# Dual Stage
|
||
self.dual_stage_model = params['dual_stage_model']
|
||
if self.dual_stage_model is not None:
|
||
# Preamp
|
||
self.preamp_variety = params['preamp_variety']
|
||
self.preamp_type_def = params['preamp_type_def']
|
||
self.preamp_nf_model = params['preamp_nf_model']
|
||
self.preamp_nf_fit_coeff = params['preamp_nf_fit_coeff']
|
||
self.preamp_gain_min = params['preamp_gain_min']
|
||
self.preamp_gain_flatmax = params['preamp_gain_flatmax']
|
||
|
||
# Booster
|
||
self.booster_variety = params['booster_variety']
|
||
self.booster_type_def = params['booster_type_def']
|
||
self.booster_nf_model = params['booster_nf_model']
|
||
self.booster_nf_fit_coeff = params['booster_nf_fit_coeff']
|
||
self.booster_gain_min = params['booster_gain_min']
|
||
self.booster_gain_flatmax = params['booster_gain_flatmax']
|
||
|
||
# Others
|
||
self.pmd = params['pmd']
|
||
self.pdl = params['pdl']
|
||
self.raman = params['raman']
|
||
self.dgt = params['dgt']
|
||
self.advance_configurations_from_json = params['advance_configurations_from_json']
|
||
|
||
# Design
|
||
self.allowed_for_design = params['allowed_for_design']
|
||
|
||
except KeyError as e:
|
||
raise ParametersError(f'Edfa configurations json must include {e}. Configuration: {params}')
|
||
|
||
def update_params(self, kwargs):
|
||
for k, v in kwargs.items():
|
||
setattr(self, k, v)
|
||
|
||
|
||
class EdfaOperational:
|
||
default_values = {
|
||
'gain_target': None,
|
||
'delta_p': None,
|
||
'out_voa': None,
|
||
'in_voa': 0,
|
||
'tilt_target': None
|
||
}
|
||
|
||
def __init__(self, **operational):
|
||
self.update_attr(operational)
|
||
|
||
def update_attr(self, kwargs):
|
||
clean_kwargs = {k: v for k, v in kwargs.items() if v != ''}
|
||
for k, v in self.default_values.items():
|
||
setattr(self, k, clean_kwargs.get(k, v))
|
||
|
||
def __repr__(self):
|
||
return (f'{type(self).__name__}('
|
||
f'gain_target={self.gain_target!r}, '
|
||
f'tilt_target={self.tilt_target!r})')
|
||
|
||
|
||
DEFAULT_EDFA_CONFIG = {
|
||
"nf_ripple": [
|
||
0.0
|
||
],
|
||
"gain_ripple": [
|
||
0.0
|
||
],
|
||
"f_min": 191.275e12,
|
||
"f_max": 196.125e12,
|
||
"dgt": [
|
||
1.0, 1.017807767853702, 1.0356155337864215, 1.0534217504465226, 1.0712204022764056, 1.0895983485572227,
|
||
1.108555289615659, 1.1280891949729075, 1.1476135933863398, 1.1672278304018044, 1.1869318618366975,
|
||
1.2067249615595257, 1.2264996957264114, 1.2428104897182262, 1.2556591482982988, 1.2650555289898042,
|
||
1.2744470198196236, 1.2838336236692311, 1.2932153453410835, 1.3040618749785347, 1.316383926863083,
|
||
1.3301807335621048, 1.3439818461440451, 1.3598972673004606, 1.3779439775587023, 1.3981208704326855,
|
||
1.418273806730323, 1.4340878115214444, 1.445565137158368, 1.45273959485914, 1.4599103316162523,
|
||
1.4670307626366115, 1.474100442252211, 1.48111939735681, 1.488134243479226, 1.495145456062699,
|
||
1.502153039909686, 1.5097346239790443, 1.5178910621476225, 1.5266220576235803, 1.5353620432989845,
|
||
1.545374152761467, 1.5566577309558969, 1.569199764184379, 1.5817353179379183, 1.5986915141218316,
|
||
1.6201194134191075, 1.6460167077689267, 1.6719047669939942, 1.6918150918099673, 1.7057507692361864,
|
||
1.7137640932265894, 1.7217732861435076, 1.7297783508684146, 1.737780757913635, 1.7459181197626403,
|
||
1.7541903672600494, 1.7625959636196327, 1.7709972329654864, 1.7793941781790852, 1.7877868031023945,
|
||
1.7961751115773796, 1.8045606557581335, 1.8139629377087627, 1.824381436842932, 1.835814081380705,
|
||
1.847275503201129, 1.862235672444246, 1.8806927939516411, 1.9026104247588487, 1.9245345552113182,
|
||
1.9482128147680253, 1.9736443063300082, 2.0008103857988204, 2.0279625371819305, 2.055100772005235,
|
||
2.082225099873648, 2.1183028432496016, 2.16337565384239, 2.2174389328192197, 2.271520771371253,
|
||
2.322373696229342, 2.3699990328716107, 2.414398437185221, 2.4587748041127506, 2.499446286796604,
|
||
2.5364027376452056, 2.5696460593920065, 2.602860350286428, 2.630396440815385, 2.6521732021128046,
|
||
2.6681935771243177, 2.6841217449620203, 2.6947834587664494, 2.705443819238505, 2.714526681131686
|
||
]
|
||
}
|
||
|
||
|
||
class MultiBandParams:
|
||
default_values = {
|
||
'bands': [],
|
||
'type_variety': '',
|
||
'type_def': None,
|
||
'allowed_for_design': False
|
||
}
|
||
|
||
def __init__(self, **params):
|
||
try:
|
||
self.update_attr(params)
|
||
except KeyError as e:
|
||
raise ParametersError(f'Multiband configurations json must include {e}. Configuration: {params}')
|
||
|
||
def update_attr(self, kwargs):
|
||
clean_kwargs = {k: v for k, v in kwargs.items() if v != ''}
|
||
for k, v in self.default_values.items():
|
||
# use deepcopy to avoid sharing same object amongst all instance when v is a list or a dict!
|
||
if isinstance(v, (list, dict)):
|
||
setattr(self, k, clean_kwargs.get(k, deepcopy(v)))
|
||
else:
|
||
setattr(self, k, clean_kwargs.get(k, v))
|
||
|
||
|
||
class TransceiverParams:
|
||
def __init__(self, **params):
|
||
self.design_bands = params.get('design_bands', [])
|
||
self.per_degree_design_bands = params.get('per_degree_design_bands', {})
|
||
|
||
|
||
@dataclass
|
||
class FrequencyBand:
|
||
"""Frequency band
|
||
"""
|
||
f_min: float
|
||
f_max: float
|
||
|
||
|
||
DEFAULT_BANDS_DEFINITION = {
|
||
"LBAND": FrequencyBand(f_min=187e12, f_max=189e12),
|
||
"CBAND": FrequencyBand(f_min=191.3e12, f_max=196.0e12)
|
||
}
|
||
# use this definition to index amplifiers'element of a multiband amplifier.
|
||
# this is not the design band
|
||
|
||
|
||
def find_band_name(band: FrequencyBand) -> str:
|
||
"""return the default band name (CBAND, LBAND, ...) that corresponds to the band frequency range
|
||
Use the band center frequency: if center frequency is inside the band then returns CBAND.
|
||
This is to flexibly encompass all kind of bands definitions.
|
||
returns the first matching band name.
|
||
"""
|
||
for band_name, frequency_range in DEFAULT_BANDS_DEFINITION.items():
|
||
center_frequency = (band.f_min + band.f_max) / 2
|
||
if center_frequency >= frequency_range.f_min and center_frequency <= frequency_range.f_max:
|
||
return band_name
|
||
return 'unknown_band'
|