diff --git a/gnpy/core/info.py b/gnpy/core/info.py index 3fbb52e1..f5a58e65 100644 --- a/gnpy/core/info.py +++ b/gnpy/core/info.py @@ -12,6 +12,7 @@ from __future__ import annotations from collections import namedtuple from collections.abc import Iterable from typing import Union +from dataclasses import dataclass from numpy import argsort, mean, array, append, ones, ceil, any, zeros, outer, full, ndarray, asarray from gnpy.core.utils import automatic_nch, db2lin, watt2dbm @@ -292,3 +293,50 @@ def create_input_spectral_information(f_min, f_max, roll_off, baud_rate, power, return create_arbitrary_spectral_information(frequency, slot_width=spacing, signal=power, baud_rate=baud_rate, roll_off=roll_off, delta_pdb_per_channel=delta_pdb_per_channel, ref_power=Pref(p_span0=p_span0, p_spani=p_spani)) + + +def carriers_to_spectral_information(initial_spectrum: dict[Union[int, float], Carrier], + ref_carrier: ReferenceCarrier) -> SpectralInformation: + """Initial spectrum is a dict with key = carrier frequency, and value a Carrier object. + :param initial_spectrum: indexed by frequency in Hz, with power offset (delta_pdb), baudrate, slot width + and roll off. + :param ref_carrier: reference carrier (baudrate and power) used for the reference channel + """ + frequency = list(initial_spectrum.keys()) + signal = [ref_carrier.req_power * db2lin(c.delta_pdb) for c in initial_spectrum.values()] + roll_off = [c.roll_off for c in initial_spectrum.values()] + baud_rate = [c.baud_rate for c in initial_spectrum.values()] + delta_pdb_per_channel = array([c.delta_pdb for c in initial_spectrum.values()]) + slot_width = [c.slot_width for c in initial_spectrum.values()] + p_span0 = watt2dbm(ref_carrier.req_power) + p_spani = watt2dbm(ref_carrier.req_power) + return create_arbitrary_spectral_information(frequency=frequency, signal=signal, baud_rate=baud_rate, + slot_width=slot_width, roll_off=roll_off, + delta_pdb_per_channel=delta_pdb_per_channel, + ref_power=Pref(p_span0=p_span0, p_spani=p_spani)) + + +@dataclass +class Carrier: + """One channel in the initial mixed-type spectrum definition, each type being defined by + its delta_pdb (power offset with respect to reference power), baud rate, slot_width, roll_off + and tx_osnr. delta_pdb offset is applied to target power out of Roadm. + """ + delta_pdb: float + baud_rate: float + slot_width: float + roll_off: float + + +@dataclass +class ReferenceCarrier: + """Reference channel is used during autodesign to determine target power + based on power spectral density values during propagation in ROADMs for equalization purpose. + It is also required to correctly compute the loss experienced by p_span_i in Roadm element. + + In typical scenarios, users would pick a 32 GBaud channel at 0dBm, which will + neatly lead to the same power spectral density for a 64 GBaud channel at 3 dBm. + Other attributes (like slot_width or roll-off) may be added there for future equalization purpose. + """ + baud_rate: float + req_power: float diff --git a/gnpy/topology/request.py b/gnpy/topology/request.py index c95d8592..65ec5dd2 100644 --- a/gnpy/topology/request.py +++ b/gnpy/topology/request.py @@ -23,7 +23,7 @@ from networkx.utils import pairwise from numpy import mean, argmin from gnpy.core.elements import Transceiver, Roadm from gnpy.core.utils import lin2db -from gnpy.core.info import create_input_spectral_information +from gnpy.core.info import create_input_spectral_information, carriers_to_spectral_information, ReferenceCarrier from gnpy.core.exceptions import ServiceError, DisjunctionError import gnpy.core.ansi_escapes as ansi_escapes from copy import deepcopy @@ -72,6 +72,7 @@ class PathRequest: if params.effective_freq_slot is not None: self.N = params.effective_freq_slot['N'] self.M = params.effective_freq_slot['M'] + self.initial_spectrum = None def __str__(self): return '\n\t'.join([f'{type(self).__name__} {self.request_id}', @@ -339,10 +340,24 @@ def compute_constrained_path(network, req): return total_path +def ref_carrier(req_power, equipment): + """Create a reference carier based SI information with the specified request's power: + req_power records the power in W that the user has defined for a given request + (which might be different from the one used for the design). + """ + return ReferenceCarrier(baud_rate=equipment['SI']['default'].baud_rate, req_power=req_power) + + def propagate(path, req, equipment): - si = create_input_spectral_information( - req.f_min, req.f_max, req.roll_off, req.baud_rate, - req.power, req.spacing) + """ propagates signals in each element according to initial spectrum set by user + """ + if req.initial_spectrum is not None: + si = carriers_to_spectral_information(initial_spectrum=req.initial_spectrum, + ref_carrier=ref_carrier(req.power, equipment)) + else: + si = create_input_spectral_information( + req.f_min, req.f_max, req.roll_off, req.baud_rate, + req.power, req.spacing) for i, el in enumerate(path): if isinstance(el, Roadm): si = el(si, degree=path[i+1].uid) @@ -378,9 +393,15 @@ def propagate_and_optimize_mode(path, req, equipment): # step2: computes propagation for each baudrate: stop and select the first that passes # TODO: the case of roll of is not included: for now use SI one # TODO: if the loop in mode optimization does not have a feasible path, then bugs - spc_info = create_input_spectral_information(req.f_min, req.f_max, - equipment['SI']['default'].roll_off, - this_br, req.power, req.spacing) + if req.initial_spectrum is not None: + # this case is not yet handled: spectrum can not be defined for the path-request-run function + # and this function is only called in this case. so coming here should not be considered yet. + msg = f'Request: {req.request_id} contains a unexpected initial_spectrum.' + LOGGER.critical(msg) + raise ServiceError(msg) + spc_info = create_input_spectral_information(f_min=req.f_min, f_max=req.f_max, + roll_off=equipment['SI']['default'].roll_off, + baud_rate=this_br, power=req.power, spacing=req.spacing) for i, el in enumerate(path): if isinstance(el, Roadm): spc_info = el(spc_info, degree=path[i+1].uid)