def test_compare_nf_models(gain, setup_edfa_variable_gain, si): """ compare the 2 amplifier models (polynomial and estimated from nf_min and max) => nf_model vs nf_poly_fit for intermediate gain values: between gain_min and gain_flatmax some discrepancy is expected but target < 0.5dB => unitary test for Edfa._calc_nf (and Edfa.interpol_params)""" edfa = setup_edfa_variable_gain frequencies = array([c.frequency for c in si.carriers]) pin = array([c.power.signal+c.power.nli+c.power.ase for c in si.carriers]) pin = pin/db2lin(gain) baud_rates = array([c.baud_rate for c in si.carriers]) edfa.operational.gain_target = gain pref = Pref(0, -gain) edfa.interpol_params(frequencies, pin, baud_rates, pref) nf_model = edfa.nf[0] edfa.interpol_params(frequencies, pin, baud_rates, pref) nf_poly = edfa.nf[0] assert pytest.approx(nf_model, abs=0.5) == nf_poly
def propagate(self, pref, *carriers): #pin_target and loss are read from eqpt_config.json['Roadm'] #all ingress channels in xpress are set to this power level #but add channels are not, so we define an effective loss #in the case of add channels self.effective_pch_out_db = min(pref.p_spani, self.params.target_pch_out_db) self.effective_loss = pref.p_spani - self.effective_pch_out_db carriers_power = array([c.power.signal +c.power.nli+c.power.ase for c in carriers]) carriers_att = list(map(lambda x : lin2db(x*1e3)-self.params.target_pch_out_db, carriers_power)) exceeding_att = -min(list(filter(lambda x: x < 0, carriers_att)), default = 0) carriers_att = list(map(lambda x: db2lin(x+exceeding_att), carriers_att)) for carrier_att, carrier in zip(carriers_att, carriers) : pwr = carrier.power pwr = pwr._replace( signal = pwr.signal/carrier_att, nli = pwr.nli/carrier_att, ase = pwr.ase/carrier_att) yield carrier._replace(power=pwr)
def interpol_params(self, frequencies, pin, baud_rates, pref): """interpolate SI channel frequencies with the edfa dgt and gain_ripple frquencies from JSON """ # TODO|jla: read amplifier actual frequencies from additional params in json self.channel_freq = frequencies amplifier_freq = arrange_frequencies(len(self.params.dgt), self.params.f_min, self.params.f_max) # Hz self.interpol_dgt = interp(self.channel_freq, amplifier_freq, self.params.dgt) amplifier_freq = arrange_frequencies(len(self.params.gain_ripple), self.params.f_min, self.params.f_max) # Hz self.interpol_gain_ripple = interp(self.channel_freq, amplifier_freq, self.params.gain_ripple) amplifier_freq = arrange_frequencies(len(self.params.nf_ripple), self.params.f_min, self.params.f_max) # Hz self.interpol_nf_ripple = interp(self.channel_freq, amplifier_freq, self.params.nf_ripple) self.nch = frequencies.size self.pin_db = lin2db(sum(pin * 1e3)) """in power mode: delta_p is defined and can be used to calculate the power target This power target is used calculate the amplifier gain""" if self.delta_p is not None: self.target_pch_out_db = round(self.delta_p + pref.p_span0, 2) self.effective_gain = self.target_pch_out_db - pref.p_spani """check power saturation and correct effective gain & power accordingly:""" self.effective_gain = min( self.effective_gain, self.params.p_max - (pref.p_spani + pref.neq_ch)) #print(self.uid, self.effective_gain, self.operational.gain_target) self.effective_pch_out_db = round(pref.p_spani + self.effective_gain, 2) """check power saturation and correct target_gain accordingly:""" #print(self.uid, self.effective_gain, self.pin_db, pref.p_spani) self.nf = self._calc_nf() self.gprofile = self._gain_profile(pin) pout = (pin + self.noise_profile(baud_rates)) * db2lin(self.gprofile) self.pout_db = lin2db(sum(pout * 1e3))
def test_compare_nf_models(gain, setup_edfa_variable_gain, si): """ compare the 2 amplifier models (polynomial and estimated from nf_min and max) => nf_model vs nf_poly_fit for intermediate gain values: between gain_min and gain_flatmax some discrepancy is expected but target < 0.5dB => unitary test for Edfa._calc_nf (and Edfa.interpol_params)""" edfa = setup_edfa_variable_gain frequencies = array([c.frequency for c in si.carriers]) pin = array( [c.power.signal + c.power.nli + c.power.ase for c in si.carriers]) pin = pin / db2lin(gain) baud_rates = array([c.baud_rate for c in si.carriers]) edfa.operational.gain_target = gain # edfa is variable gain type pref = Pref(0, -gain, lin2db(len(frequencies))) edfa.interpol_params(frequencies, pin, baud_rates, pref) nf_model = edfa.nf[0] # change edfa type variety to a polynomial el_config = { "uid": "Edfa1", "operational": { "gain_target": gain, "tilt_target": 0 }, "metadata": { "location": { "region": "", "latitude": 2, "longitude": 0 } } } equipment = load_equipment(eqpt_library) extra_params = equipment['Edfa']['CienaDB_medium_gain'] temp = el_config.setdefault('params', {}) temp = merge_amplifier_restrictions(temp, extra_params.__dict__) el_config['params'] = temp edfa = Edfa(**el_config) # edfa is variable gain type edfa.interpol_params(frequencies, pin, baud_rates, pref) nf_poly = edfa.nf[0] print(nf_poly, nf_model) assert pytest.approx(nf_model, abs=0.5) == nf_poly
def noise_profile(self, df): """noise_profile(bw) computes amplifier ASE (W) in signal bandwidth (Hz) Noise is calculated at amplifier input :bw: signal bandwidth = baud rate in Hz :type bw: float :return: the asepower in W in the signal bandwidth bw for 96 channels :return type: numpy array of float ASE power using per channel gain profile inputs: NF_dB - Noise figure in dB, vector of length number of channels or spectral slices G_dB - Actual gain calculated for the EDFA, vector of length number of channels or spectral slices ffs - Center frequency grid of the channels or spectral slices in THz, vector of length number of channels or spectral slices dF - width of each channel or spectral slice in THz, vector of length number of channels or spectral slices OUTPUT: ase_dBm - ase in dBm per channel or spectral slice NOTE: The output is the total ASE in the channel or spectral slice. For 50GHz channels the ASE BW is effectively 0.4nm. To get to noise power in 0.1nm, subtract 6dB. ONSR is usually quoted as channel power divided by the ASE power in 0.1nm RBW, regardless of the width of the actual channel. This is a historical convention from the days when optical signals were much smaller (155Mbps, 2.5Gbps, ... 10Gbps) than the resolution of the OSAs that were used to measure spectral power which were set to 0.1nm resolution for convenience. Moving forward into flexible grid and high baud rate signals, it may be convenient to begin quoting power spectral density in the same BW for both signal and ASE, e.g. 12.5GHz.""" ase = h * df * self.channel_freq * db2lin(self.nf) # W return ase # in W at amplifier input
def propagate(self, pref, *carriers): #pin_target and loss are read from eqpt_config.json['Roadm'] #all ingress channels in xpress are set to this power level #but add channels are not, so we define an effective loss #in the case of add channels if self.target_pch_out_db: self.effective_loss = pref.pi - self.target_pch_out_db else: self.effective_loss = self.loss self.effective_pch_out_db = pref.pi - self.effective_loss attenuation = db2lin(self.effective_loss) for carrier in carriers: pwr = carrier.power pwr = pwr._replace(signal=pwr.signal / attenuation, nonlinear_interference=pwr.nli / attenuation, amplified_spontaneous_emission=pwr.ase / attenuation) yield carrier._replace(power=pwr)
def gsnr_01(data_path): config_path = data_path / 'config_file.json' config = json.load(open(config_path, 'r')) list_data_files = [ file for file in os.listdir(data_path) if file.endswith('.mat') ] baud_rate = config['spectral_config']['general_si']['symbol_rates'] ratio_01nm = lin2db(12.5e9 / baud_rate) for file in list_data_files: data = DataQot.load_qot_mat(data_path / file) gsnr_01_list = [] for i, gsnr_lin in enumerate(data.gsnr): gsnr = lin2db(gsnr_lin) - ratio_01nm gsnr_01_list.append(db2lin(gsnr)) data.gsnr = np.array(gsnr_01_list) data.save_data()
def update_snr(self, *args): """ snr_added in 0.1nm compute SNR penalties such as transponder Tx_osnr or Roadm add_drop_osnr only applied in request.py / propagate on the last Trasceiver node of the path all penalties are added in a single call because to avoid uncontrolled cumul """ # use raw_values so that the added SNR penalties are not cumulated snr_added = 0 for s in args: snr_added += db2lin(-s) snr_added = -lin2db(snr_added) self.osnr_ase = list(map(lambda x, y: snr_sum(x, y, snr_added), self.raw_osnr_ase, self.baud_rate)) self.snr = list(map(lambda x, y: snr_sum(x, y, snr_added), self.raw_snr, self.baud_rate)) self.osnr_ase_01nm = list(map(lambda x: snr_sum(x, 12.5e9, snr_added), self.raw_osnr_ase_01nm)) self.snr_01nm = list(map(lambda x: snr_sum(x, 12.5e9, snr_added), self.raw_snr_01nm))
def propagation(input_power, con_in, con_out,dest): equipment = load_equipment(eqpt_library_name) network = load_network(network_file_name,equipment) build_network(network, equipment, 0, 20) # parametrize the network elements with the con losses and adapt gain # (assumes all spans are identical) for e in network.nodes(): if isinstance(e, Fiber): loss = e.loss_coef * e.length e.con_in = con_in e.con_out = con_out if isinstance(e, Edfa): e.operational.gain_target = loss + con_in + con_out transceivers = {n.uid: n for n in network.nodes() if isinstance(n, Transceiver)} p = input_power p = db2lin(p) * 1e-3 spacing = 0.05 # THz si = SpectralInformation() # SI units: W, Hz si = si.update(carriers=[ Channel(f, (191.3 + spacing * f) * 1e12, 32e9, 0.15, Power(p, 0, 0)) for f in range(1,80) ]) source = next(transceivers[uid] for uid in transceivers if uid == 'trx A') sink = next(transceivers[uid] for uid in transceivers if uid == dest) path = dijkstra_path(network, source, sink) for el in path: si = el(si) print(el) # remove this line when sweeping across several powers edfa_sample = next(el for el in path if isinstance(el, Edfa)) nf = mean(edfa_sample.nf) print(f'pw: {input_power} conn in: {con_in} con out: {con_out}', f'[email protected]: {round(mean(sink.osnr_ase_01nm),2)}', f'SNR@bandwitdth: {round(mean(sink.snr),2)}') return sink , nf
def propagate(self, *carriers): """add ase noise to the propagating carriers of SpectralInformation""" i = 0 pin = np.array([ c.power.signal + c.power.nli + c.power.ase for c in carriers ]) #pin in W freq = np.array([c.frequency for c in carriers]) brate = np.array([c.baud_rate for c in carriers]) #interpolate the amplifier vectors with the carriers freq, calculate nf & gain profile self.interpol_params(freq, pin, brate) gain = db2lin(self.gprofile) carrier_ase = self.noise_profile(brate) for carrier in carriers: pwr = carrier.power bw = carrier.baud_rate pwr = pwr._replace( signal=pwr.signal * gain[i], nonlinear_interference=pwr.nli * gain[i], amplified_spontaneous_emission=(pwr.ase + carrier_ase[i]) * gain[i]) i += 1 yield carrier._replace(power=pwr)
def propagate(self, pref, *carriers, degree): # pin_target and loss are read from eqpt_config.json['Roadm'] # all ingress channels in xpress are set to this power level # but add channels are not, so we define an effective loss # in the case of add channels # find the target power on this degree: # if a target power has been defined for this degree use it else use the global one. # if the input power is lower than the target one, use the input power instead because # a ROADM doesn't amplify, it can only attenuate # TODO maybe add a minimum loss for the ROADM per_degree_pch = self.per_degree_pch_out_db[degree] if degree in self.per_degree_pch_out_db.keys() else self.params.target_pch_out_db self.effective_pch_out_db = min(pref.p_spani, per_degree_pch) self.effective_loss = pref.p_spani - self.effective_pch_out_db carriers_power = array([c.power.signal + c.power.nli + c.power.ase for c in carriers]) carriers_att = list(map(lambda x: lin2db(x * 1e3) - per_degree_pch, carriers_power)) exceeding_att = -min(list(filter(lambda x: x < 0, carriers_att)), default=0) carriers_att = list(map(lambda x: db2lin(x + exceeding_att), carriers_att)) for carrier_att, carrier in zip(carriers_att, carriers): pwr = carrier.power pwr = pwr._replace(signal=pwr.signal / carrier_att, nli=pwr.nli / carrier_att, ase=pwr.ase / carrier_att) pmd = sqrt(carrier.pmd**2 + self.params.pmd**2) yield carrier._replace(power=pwr, pmd=pmd)
def estimate_nf_model(type_variety, gain_min, gain_max, nf_min, nf_max): if nf_min < -10: raise EquipmentConfigError( f'Invalid nf_min value {nf_min!r} for amplifier {type_variety}') if nf_max < -10: raise EquipmentConfigError( f'Invalid nf_max value {nf_max!r} for amplifier {type_variety}') # NF estimation model based on nf_min and nf_max # delta_p: max power dB difference between first and second stage coils # dB g1a: first stage gain - internal VOA attenuation # nf1, nf2: first and second stage coils # calculated by solving nf_{min,max} = nf1 + nf2 / g1a{min,max} delta_p = 5 g1a_min = gain_min - (gain_max - gain_min) - delta_p g1a_max = gain_max - delta_p nf2 = lin2db((db2lin(nf_min) - db2lin(nf_max)) / (1 / db2lin(g1a_max) - 1 / db2lin(g1a_min))) nf1 = lin2db(db2lin(nf_min) - db2lin(nf2) / db2lin(g1a_max)) if nf1 < 4: raise EquipmentConfigError( f'First coil value too low {nf1} for amplifier {type_variety}') # Check 1 dB < delta_p < 6 dB to ensure nf_min and nf_max values make sense. # There shouldn't be high nf differences between the two coils: # nf2 should be nf1 + 0.3 < nf2 < nf1 + 2 # If not, recompute and check delta_p if not nf1 + 0.3 < nf2 < nf1 + 2: nf2 = clip(nf2, nf1 + 0.3, nf1 + 2) g1a_max = lin2db(db2lin(nf2) / (db2lin(nf_min) - db2lin(nf1))) delta_p = gain_max - g1a_max g1a_min = gain_min - (gain_max - gain_min) - delta_p if not 1 < delta_p < 11: raise EquipmentConfigError( f'Computed \N{greek capital letter delta}P invalid \ \n 1st coil vs 2nd coil calculated DeltaP {delta_p:.2f} for \ \n amplifier {type_variety} is not valid: revise inputs \ \n calculated 1st coil NF = {nf1:.2f}, 2nd coil NF = {nf2:.2f}' ) # Check calculated values for nf1 and nf2 calc_nf_min = lin2db(db2lin(nf1) + db2lin(nf2) / db2lin(g1a_max)) if not isclose(nf_min, calc_nf_min, abs_tol=0.01): raise EquipmentConfigError( f'nf_min does not match calc_nf_min, {nf_min} vs {calc_nf_min} for amp {type_variety}' ) calc_nf_max = lin2db(db2lin(nf1) + db2lin(nf2) / db2lin(g1a_min)) if not isclose(nf_max, calc_nf_max, abs_tol=0.01): raise EquipmentConfigError( f'nf_max does not match calc_nf_max, {nf_max} vs {calc_nf_max} for amp {type_variety}' ) return nf1, nf2, delta_p
def propagate_raman_fiber(fiber, *carriers): simulation = Simulation.get_simulation() sim_params = simulation.sim_params raman_params = sim_params.raman_params nli_params = sim_params.nli_params # apply input attenuation to carriers attenuation_in = db2lin(fiber.params.con_in + fiber.params.att_in) chan = [] for carrier in carriers: pwr = carrier.power pwr = pwr._replace(signal=pwr.signal / attenuation_in, nli=pwr.nli / attenuation_in, ase=pwr.ase / attenuation_in) carrier = carrier._replace(power=pwr) chan.append(carrier) carriers = tuple(f for f in chan) # evaluate fiber attenuation involving also SRS if required by sim_params raman_solver = fiber.raman_solver raman_solver.carriers = carriers raman_solver.raman_pumps = fiber.raman_pumps stimulated_raman_scattering = raman_solver.stimulated_raman_scattering fiber_attenuation = (stimulated_raman_scattering.rho[:, -1])**-2 if not raman_params.flag_raman: fiber_attenuation = tuple(fiber.params.lin_attenuation for _ in carriers) # evaluate Raman ASE noise if required by sim_params and if raman pumps are present if raman_params.flag_raman and fiber.raman_pumps: raman_ase = raman_solver.spontaneous_raman_scattering.power[:, -1] else: raman_ase = tuple(0 for _ in carriers) # evaluate nli and propagate in fiber attenuation_out = db2lin(fiber.params.con_out) nli_solver = fiber.nli_solver nli_solver.stimulated_raman_scattering = stimulated_raman_scattering nli_frequencies = [] computed_nli = [] for carrier in ( c for c in carriers if c.channel_number in sim_params.nli_params.computed_channels): resolution_param = frequency_resolution(carrier, carriers, sim_params, fiber) f_cut_resolution, f_pump_resolution, _, _ = resolution_param nli_params.f_cut_resolution = f_cut_resolution nli_params.f_pump_resolution = f_pump_resolution nli_frequencies.append(carrier.frequency) computed_nli.append(nli_solver.compute_nli(carrier, *carriers)) new_carriers = [] for carrier, attenuation, rmn_ase in zip(carriers, fiber_attenuation, raman_ase): carrier_nli = interp(carrier.frequency, nli_frequencies, computed_nli) pwr = carrier.power pwr = pwr._replace( signal=pwr.signal / attenuation / attenuation_out, nli=(pwr.nli + carrier_nli) / attenuation / attenuation_out, ase=((pwr.ase / attenuation) + rmn_ase) / attenuation_out) new_carriers.append(carrier._replace(power=pwr)) return new_carriers
def transmission_main_example(args=None): 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 summary') parser.add_argument('-pl', '--plot', action='store_true') parser.add_argument('-l', '--list-nodes', action='store_true', help='list all transceiver nodes') parser.add_argument('-po', '--power', default=0, help='channel ref power in dBm') parser.add_argument('source', nargs='?', help='source node') parser.add_argument('destination', nargs='?', help='destination node') args = parser.parse_args(args if args is not None else sys.argv[1:]) _setup_logging(args) (equipment, network) = load_common_data(args.equipment, args.topology, args.sim_params, args.save_network_before_autodesign) if args.plot: plot_baseline(network) transceivers = { n.uid: n for n in network.nodes() if isinstance(n, Transceiver) } if not transceivers: sys.exit('Network has no transceivers!') if len(transceivers) < 2: sys.exit('Network has only one transceiver!') if args.list_nodes: for uid in transceivers: print(uid) sys.exit() # First try to find exact match if source/destination provided if args.source: source = transceivers.pop(args.source, None) valid_source = True if source else False else: source = None _logger.info('No source node specified: picking random transceiver') if args.destination: destination = transceivers.pop(args.destination, None) valid_destination = True if destination else False else: destination = None _logger.info( 'No destination node specified: picking random transceiver') # If no exact match try to find partial match if args.source and not source: # TODO code a more advanced regex to find nodes match source = next( (transceivers.pop(uid) for uid in transceivers if args.source.lower() in uid.lower()), None) if args.destination and not destination: # TODO code a more advanced regex to find nodes match destination = next((transceivers.pop(uid) for uid in transceivers if args.destination.lower() in uid.lower()), None) # If no partial match or no source/destination provided pick random if not source: source = list(transceivers.values())[0] del transceivers[source.uid] if not destination: destination = list(transceivers.values())[0] _logger.info(f'source = {args.source!r}') _logger.info(f'destination = {args.destination!r}') params = {} params['request_id'] = 0 params['trx_type'] = '' params['trx_mode'] = '' params['source'] = source.uid params['destination'] = destination.uid params['bidir'] = False params['nodes_list'] = [destination.uid] params['loose_list'] = ['strict'] params['format'] = '' params['path_bandwidth'] = 0 trx_params = trx_mode_params(equipment) # Randomly generate input power input_power = round(random.random(), 2) input_power = -2 + (input_power * (8)) args.power = input_power if args.power: trx_params['power'] = db2lin(float(args.power)) * 1e-3 params.update(trx_params) req = PathRequest(**params) power_mode = equipment['Span']['default'].power_mode print('\n'.join([ f'Power mode is set to {power_mode}', f'=> it can be modified in eqpt_config.json - Span' ])) pref_ch_db = lin2db(req.power * 1e3) # reference channel power / span (SL=20dB) pref_total_db = pref_ch_db + lin2db( req.nb_channel) # reference total power / span (SL=20dB) try: build_network(network, equipment, pref_ch_db, pref_total_db) except exceptions.NetworkTopologyError as e: print( f'{ansi_escapes.red}Invalid network definition:{ansi_escapes.reset} {e}' ) sys.exit(1) except exceptions.ConfigurationError as e: print( f'{ansi_escapes.red}Configuration error:{ansi_escapes.reset} {e}') sys.exit(1) path = compute_constrained_path(network, req) 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} ' f'and {destination.uid}') print(f'\nNow propagating between {source.uid} and {destination.uid}:') try: p_start, p_stop, p_step = equipment['SI']['default'].power_range_db p_num = abs(int(round( (p_stop - p_start) / p_step))) + 1 if p_step != 0 else 1 power_range = list(linspace(p_start, p_stop, p_num)) except TypeError: print( 'invalid power range definition in eqpt_config, should be power_range_db: [lower, upper, step]' ) power_range = [0] if not power_mode: # power cannot be changed in gain mode power_range = [0] for dp_db in power_range: req.power = db2lin(pref_ch_db + dp_db) * 1e-3 if power_mode: print( f'\nPropagating with input power = {ansi_escapes.cyan}{lin2db(req.power*1e3):.2f} dBm{ansi_escapes.reset}:' ) else: print( f'\nPropagating in {ansi_escapes.cyan}gain mode{ansi_escapes.reset}: power cannot be set manually' ) infos = propagate(path, req, equipment) if len(power_range) == 1: for elem in path: print(elem) if power_mode: print( f'\nTransmission result for input power = {lin2db(req.power*1e3):.2f} dBm:' ) else: print(f'\nTransmission results:') # print('-------------') # print(destination.snr_01nm) # print('-------------') print( f' Final SNR total (0.1 nm): {ansi_escapes.cyan}{mean(destination.snr_01nm):.02f} dB{ansi_escapes.reset}' ) else: print(path[-1]) if args.save_network is not None: save_network(network, args.save_network) print( f'{ansi_escapes.blue}Network (after autodesign) saved to {args.save_network}{ansi_escapes.reset}' ) if args.show_channels: print('\nThe total SNR per channel at the end of the line is:') print('{:>5}{:>26}{:>26}{:>28}{:>28}{:>28}'.format( 'Ch. #', 'Channel frequency (THz)', 'Channel power (dBm)', 'OSNR ASE (signal bw, dB)', 'SNR NLI (signal bw, dB)', 'SNR total (signal bw, dB)')) # print(dir(info)) for final_carrier, ch_osnr, ch_snr_nl, ch_snr in zip( infos.carriers, path[-1].osnr_ase, path[-1].osnr_nli, path[-1].snr): ch_freq = final_carrier.frequency * 1e-12 ch_power = lin2db(final_carrier.power.signal * 1e3) print('{:5}{:26.2f}{:26.2f}{:28.2f}{:28.2f}{:28.2f}'.format( final_carrier.channel_number, round(ch_freq, 2), round(ch_power, 2), round(ch_osnr, 2), round(ch_snr_nl, 2), round(ch_snr, 2))) if not args.source: print(f'\n(No source node specified: picked {source.uid})') elif not valid_source: print( f'\n(Invalid source node {args.source!r} replaced with {source.uid})' ) if not args.destination: print(f'\n(No destination node specified: picked {destination.uid})') elif not valid_destination: print( f'\n(Invalid destination node {args.destination!r} replaced with {destination.uid})' ) if args.plot: plot_results(network, path, source, destination) # MY ADDITION # just to see what the different contributions of ASE and NLI are # return input_power, path[-1].osnr_ase, path[-1].osnr_nli, path[-1].snr # to test Raman return input_power, destination.snr_01nm, mean(destination.snr_01nm)
def __init__(self, Request, eqpt_filename): # 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 if not isinstance(Request.request_id, str): value = str(int(Request.request_id)) if value.endswith('.0'): value = value[:-2] self.request_id = value else: self.request_id = Request.request_id self.source = Request.source self.destination = Request.destination self.srctpid = f'trx {Request.source}' self.dsttpid = f'trx {Request.destination}' # test that trx_type belongs to eqpt_config.json # if not replace it with a default equipment = load_equipment(eqpt_filename) try: if equipment['Transceiver'][Request.trx_type]: self.trx_type = Request.trx_type if [ mode for mode in equipment['Transceiver'][Request.trx_type].mode ]: self.mode = Request.mode except KeyError: msg = f'could not find tsp : {Request.trx_type} with mode: {Request.mode} in eqpt library \nComputation stopped.' #print(msg) logger.critical(msg) exit() # excel input are in GHz and dBm self.spacing = Request.spacing * 1e9 self.power = db2lin(Request.power) * 1e-3 self.nb_channel = int(Request.nb_channel) if not isinstance(Request.disjoint_from, str): value = str(int(Request.disjoint_from)) if value.endswith('.0'): value = value[:-2] else: value = Request.disjoint_from self.disjoint_from = [n for n in value.split()] self.nodes_list = [] if Request.nodes_list: self.nodes_list = Request.nodes_list.split(' | ') try: self.nodes_list.remove(self.source) msg = f'{self.source} removed from explicit path node-list' logger.info(msg) # print(msg) except ValueError: msg = f'{self.source} already removed from explicit path node-list' logger.info(msg) # print(msg) try: self.nodes_list.remove(self.destination) msg = f'{self.destination} removed from explicit path node-list' logger.info(msg) # print(msg) except ValueError: msg = f'{self.destination} already removed from explicit path node-list' logger.info(msg) # print(msg) self.loose = 'loose' if Request.is_loose == 'no': self.loose = 'strict'
def _gain_profile(self, pin, err_tolerance=1.0e-11, simple_opt=True): """ Pin : input power / channel in W :param gain_ripple: design flat gain :param dgt: design gain tilt :param Pin: total input power in W :param gp: Average gain setpoint in dB units (provisioned gain) :param gtp: gain tilt setting (provisioned tilt) :type gain_ripple: numpy.ndarray :type dgt: numpy.ndarray :type Pin: numpy.ndarray :type gp: float :type gtp: float :return: gain profile in dBm, per channel or spectral slice :rtype: numpy.ndarray Checking of output power clamping is implemented in interpol_params(). Based on: R. di Muro, "The Er3+ fiber gain coefficient derived from a dynamic gain tilt technique", Journal of Lightwave Technology, Vol. 18, Iss. 3, Pp. 343-347, 2000. Ported from Matlab version written by David Boerges at Ciena. """ # TODO|jla: check what param should be used (currently length(dgt)) if len(self.interpol_dgt) == 1: return array([self.effective_gain]) # TODO|jla: find a way to use these or lose them. Primarily we should have # a way to determine if exceeding the gain or output power of the amp tot_in_power_db = self.pin_db # Pin in W # linear fit to get the p = polyfit(self.channel_freq, self.interpol_dgt, 1) dgt_slope = p[0] # Calculate the target slope targ_slope = -self.tilt_target / (self.params.f_max - self.params.f_min) # first estimate of DGT scaling try: dgts1 = targ_slope / dgt_slope except ZeroDivisionError: dgts1 = 0 # when simple_opt is true, make 2 attempts to compute gain and # the internal voa value. This is currently here to provide direct # comparison with original Matlab code. Will be removed. # TODO|jla: replace with loop if not simple_opt: return # first estimate of Er gain & VOA loss g1st = array(self.interpol_gain_ripple) + self.params.gain_flatmax \ + array(self.interpol_dgt) * dgts1 voa = lin2db(mean(db2lin(g1st))) - self.effective_gain # second estimate of amp ch gain using the channel input profile g2nd = g1st - voa pout_db = lin2db(sum(pin * 1e3 * db2lin(g2nd))) dgts2 = self.effective_gain - (pout_db - tot_in_power_db) # center estimate of amp ch gain xcent = dgts2 gcent = g1st - voa + array(self.interpol_dgt) * xcent pout_db = lin2db(sum(pin * 1e3 * db2lin(gcent))) gavg_cent = pout_db - tot_in_power_db # Lower estimate of amp ch gain deltax = max(g1st) - min(g1st) # if no ripple deltax = 0 and xlow = xcent: div 0 # TODO|jla: add check for flat gain response if abs(deltax) <= 0.05: # not enough ripple to consider calculation return g1st - voa xlow = dgts2 - deltax glow = g1st - voa + array(self.interpol_dgt) * xlow pout_db = lin2db(sum(pin * 1e3 * db2lin(glow))) gavg_low = pout_db - tot_in_power_db # upper gain estimate xhigh = dgts2 + deltax ghigh = g1st - voa + array(self.interpol_dgt) * xhigh pout_db = lin2db(sum(pin * 1e3 * db2lin(ghigh))) gavg_high = pout_db - tot_in_power_db # compute slope slope1 = (gavg_low - gavg_cent) / (xlow - xcent) slope2 = (gavg_cent - gavg_high) / (xcent - xhigh) if abs(self.effective_gain - gavg_cent) <= err_tolerance: dgts3 = xcent elif self.effective_gain < gavg_cent: dgts3 = xcent - (gavg_cent - self.effective_gain) / slope1 else: dgts3 = xcent + (-gavg_cent + self.effective_gain) / slope2 return g1st - voa + array(self.interpol_dgt) * dgts3
ch_spacing = 0.05 fc = itufs(ch_spacing) lc = freq2wavelength(fc) / 1000 nchan = np.arange(len(lc)) df = np.ones(len(lc)) * ch_spacing edfa1 = [n for n in nw.nodes() if n.uid == 'Edfa1'][0] edfa1.gain_target = 20.0 edfa1.tilt_target = -0.7 edfa1.calc_nf() results = [] for Pin in pch2d: chgain = edfa1.gain_profile(Pin) pase = edfa1.noise_profile(chgain, fc, df) pout = lin2db(db2lin(Pin + chgain) + db2lin(pase)) results.append(pout) # Generate legend text pch2d_legend = [] for ea in pch2d_legend_data: s = ''.join([chr(xx) for xx in ea.astype(dtype=int)]).strip() pch2d_legend.append(s) # Plot axis_font = {'fontname': 'Arial', 'size': '16', 'fontweight': 'bold'} title_font = {'fontname': 'Arial', 'size': '17', 'fontweight': 'bold'} tic_font = {'fontname': 'Arial', 'size': '12'} plt.rcParams["font.family"] = "Arial" plt.figure()
def _gain_profile(self, pin): """ Pin : input power / channel in W :param gain_ripple: design flat gain :param dgt: design gain tilt :param Pin: total input power in W :param gp: Average gain setpoint in dB units :param gtp: gain tilt setting :type gain_ripple: numpy.ndarray :type dgt: numpy.ndarray :type Pin: numpy.ndarray :type gp: float :type gtp: float :return: gain profile in dBm :rtype: numpy.ndarray AMPLIFICATION USING INPUT PROFILE INPUTS: gain_ripple - vector of length number of channels or spectral slices DGT - vector of length number of channels or spectral slices Pin - input powers vector of length number of channels or spectral slices Gp - provisioned gain length 1 GTp - provisioned tilt length 1 OUTPUT: amp gain per channel or spectral slice NOTE: there is no checking done for violations of the total output power capability of the amp. EDIT OF PREVIOUS NOTE: power violation now added in interpol_params Ported from Matlab version written by David Boerges at Ciena. Based on: R. di Muro, "The Er3+ fiber gain coefficient derived from a dynamic gain tilt technique", Journal of Lightwave Technology, Vol. 18, Iss. 3, Pp. 343-347, 2000. """ err_tolerance = 1.0e-11 simple_opt = True # TODO check what param should be used (currently length(dgt)) nchan = np.arange(len(self.interpol_dgt)) # TODO find a way to use these or lose them. Primarily we should have # a way to determine if exceeding the gain or output power of the amp tot_in_power_db = lin2db(np.sum(pin * 1e3)) # ! Pin expressed in W # Linear fit to get the p = np.polyfit(nchan, self.interpol_dgt, 1) dgt_slope = p[0] # Calculate the target slope- Currently assumes equal spaced channels # TODO make it so that supports arbitrary channel spacing. targ_slope = self.operational.tilt_target / (len(nchan) - 1) # 1st estimate of DGT scaling if abs(dgt_slope) > 0.001: # add check for div 0 due to flat dgt dgts1 = targ_slope / dgt_slope else: dgts1 = 0 # when simple_opt is true code makes 2 attempts to compute gain and # the internal voa value. This is currently here to provide direct # comparison with original Matlab code. Will be removed. # TODO replace with loop if simple_opt: # 1st estimate of Er gain & voa loss g1st = np.array(self.interpol_gain_ripple) + self.params.gain_flatmax + \ np.array(self.interpol_dgt) * dgts1 voa = lin2db(np.mean(db2lin(g1st))) - self.operational.gain_target # 2nd estimate of Amp ch gain using the channel input profile g2nd = g1st - voa pout_db = lin2db(np.sum(pin * 1e3 * db2lin(g2nd))) dgts2 = self.operational.gain_target - (pout_db - tot_in_power_db) # Center estimate of amp ch gain xcent = dgts2 gcent = g1st - voa + np.array(self.interpol_dgt) * xcent pout_db = lin2db(np.sum(pin * 1e3 * db2lin(gcent))) gavg_cent = pout_db - tot_in_power_db # Lower estimate of Amp ch gain deltax = np.max(g1st) - np.min(g1st) # ! if no ripple deltax = 0 => xlow = xcent: div 0 # add check for flat gain response : if abs( deltax ) > 0.05: #enough ripple to consider calculation and avoid div 0 xlow = dgts2 - deltax glow = g1st - voa + np.array(self.interpol_dgt) * xlow pout_db = lin2db(np.sum(pin * 1e3 * db2lin(glow))) gavg_low = pout_db - tot_in_power_db # Upper gain estimate xhigh = dgts2 + deltax ghigh = g1st - voa + np.array(self.interpol_dgt) * xhigh pout_db = lin2db(np.sum(pin * 1e3 * db2lin(ghigh))) gavg_high = pout_db - tot_in_power_db # compute slope slope1 = (gavg_low - gavg_cent) / (xlow - xcent) slope2 = (gavg_cent - gavg_high) / (xcent - xhigh) if np.abs(self.operational.gain_target - gavg_cent) <= err_tolerance: dgts3 = xcent elif self.operational.gain_target < gavg_cent: dgts3 = xcent - (gavg_cent - self.operational.gain_target) / slope1 else: dgts3 = xcent + (-gavg_cent + self.operational.gain_target) / slope2 gprofile = g1st - voa + np.array(self.interpol_dgt) * dgts3 else: #not enough ripple gprofile = g1st - voa else: #simple_opt gprofile = None return gprofile
def test_roadm_target_power(prev_node_type, effective_pch_out_db): ''' Check that egress power of roadm is equal to target power if input power is greater than target power else, that it is equal to input power. Use a simple two hops A-B-C topology for the test where the prev_node in ROADM B is either an amplifier or a fused, so that the target power can not be met in this last case. ''' equipment = load_equipment(EQPT_LIBRARY_NAME) json_network = load_json(TEST_DIR / 'data/twohops_roadm_power_test.json') prev_node = next(n for n in json_network['elements'] if n['uid'] == 'west edfa in node B to ila2') json_network['elements'].remove(prev_node) if prev_node_type == 'edfa': prev_node = {'uid': 'west edfa in node B to ila2', 'type': 'Edfa'} elif prev_node_type == 'fused': prev_node = {'uid': 'west edfa in node B to ila2', 'type': 'Fused'} prev_node['params'] = {'loss': 0} json_network['elements'].append(prev_node) network = network_from_json(json_network, equipment) # Build the network once using the default power defined in SI in eqpt config 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) params = {} params['request_id'] = 0 params['trx_type'] = '' params['trx_mode'] = '' params['source'] = 'trx node A' params['destination'] = 'trx node C' params['bidir'] = False params['nodes_list'] = ['trx node C'] params['loose_list'] = ['strict'] params['format'] = '' params['path_bandwidth'] = 100e9 trx_params = trx_mode_params(equipment) params.update(trx_params) req = PathRequest(**params) path = compute_constrained_path(network, req) 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): carriers_power_in_roadm = min([ c.power.signal + c.power.nli + c.power.ase for c in si.carriers ]) si = el(si, degree=path[i + 1].uid) if el.uid == 'roadm node B': print('input', carriers_power_in_roadm) assert el.effective_pch_out_db == effective_pch_out_db for carrier in si.carriers: print(carrier.power.signal + carrier.power.nli + carrier.power.ase) power = carrier.power.signal + carrier.power.nli + carrier.power.ase if prev_node_type == 'edfa': # edfa prev_node sets input power to roadm to a high enough value: # Check that egress power of roadm is equal to target power assert power == pytest.approx( db2lin(effective_pch_out_db - 30), rel=1e-3) elif prev_node_type == 'fused': # fused prev_node does reamplfy power after fiber propagation, so input power # to roadm is low. # Check that egress power of roadm is equalized to the min carrier input power. assert power == pytest.approx(carriers_power_in_roadm, rel=1e-3) else: si = el(si)
def __init__(self, Request, eqpt_filename): # 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 = Request.source self.destination = Request.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}' # test that trx_type belongs to eqpt_config.json # if not replace it with a default equipment = load_equipment(eqpt_filename) 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 else: msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.' #print(msg) logger.critical(msg) exit(1) else: Requestmode = None self.mode = Request.mode except KeyError: msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.' #print(msg) logger.critical(msg) exit() # excel input are in GHz and dBm if Request.spacing is not None: self.spacing = Request.spacing * 1e9 else: msg = f'Request {self.request_id} missing spacing: spacing is mandatory.\ncomputation stopped' logger.critical(msg) exit() 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 value = correct_xlrd_int_to_str_reading(Request.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(' | ') # cleaning the list of nodes to remove source and destination # (because the remaining of the program assumes that the nodes list are nodes # on the path and should not include source and destination) try: self.nodes_list.remove(self.source) msg = f'{self.source} removed from explicit path node-list' logger.info(msg) except ValueError: msg = f'{self.source} already removed from explicit path node-list' logger.info(msg) try: self.nodes_list.remove(self.destination) msg = f'{self.destination} removed from explicit path node-list' logger.info(msg) except ValueError: msg = f'{self.destination} already removed from explicit path node-list' logger.info(msg) # the excel parser applies the same hop-type to all nodes in the route nodes_list. # user can change this per node in the generated json self.loose = 'loose' if Request.is_loose == '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
def main(network, equipment, source, destination, req=None): result_dicts = {} network_data = [{ 'network_name': str(args.filename), 'source': source.uid, 'destination': destination.uid }] result_dicts.update({'network': network_data}) design_data = [{ 'power_mode': equipment['Spans']['default'].power_mode, 'span_power_range': equipment['Spans']['default'].delta_power_range_db, 'design_pch': equipment['SI']['default'].power_dbm, 'baud_rate': equipment['SI']['default'].baud_rate }] result_dicts.update({'design': design_data}) simulation_data = [] result_dicts.update({'simulation results': simulation_data}) power_mode = equipment['Spans']['default'].power_mode print('\n'.join([ f'Power mode is set to {power_mode}', f'=> it can be modified in eqpt_config.json - Spans' ])) pref_ch_db = lin2db(req.power * 1e3) #reference channel power / span (SL=20dB) pref_total_db = pref_ch_db + lin2db( req.nb_channel) #reference total power / span (SL=20dB) build_network(network, equipment, pref_ch_db, pref_total_db) path = compute_constrained_path(network, req) spans = [s.length for s in path if isinstance(s, Fiber)] print( f'\nThere are {len(spans)} fiber spans over {sum(spans):.0f}m between {source.uid} and {destination.uid}' ) print(f'\nNow propagating between {source.uid} and {destination.uid}:') try: power_range = list(arange(*equipment['SI']['default'].power_range_db)) last = equipment['SI']['default'].power_range_db[-2] if len(power_range) == 0: #bad input that will lead to no simulation power_range = [0] #better than an error message else: power_range.append(last) except TypeError: print( 'invalid power range definition in eqpt_config, should be power_range_db: [lower, upper, step]' ) power_range = [0] for dp_db in power_range: req.power = db2lin(pref_ch_db + dp_db) * 1e-3 print( f'\nPropagating with input power = {lin2db(req.power*1e3):.2f}dBm :' ) propagate(path, req, equipment, show=len(power_range) == 1) print( f'\nTransmission result for input power = {lin2db(req.power*1e3):.2f}dBm :' ) print(destination) simulation_data.append({ 'Pch_dBm': pref_ch_db + dp_db, 'OSNR_ASE_0.1nm': round(mean(destination.osnr_ase_01nm), 2), 'OSNR_ASE_signal_bw': round(mean(destination.osnr_ase), 2), 'SNR_nli_signal_bw': round(mean(destination.osnr_nli), 2), 'SNR_total_signal_bw': round(mean(destination.snr), 2) }) write_csv(result_dicts, 'simulation_result.csv') return path
def test_db2lin(): assert pytest.approx(10.0) == db2lin(10.0)
def main(network, equipment, source, destination, sim_params, req=None): result_dicts = {} network_data = [{ 'network_name' : str(args.filename), 'source' : source.uid, 'destination' : destination.uid }] result_dicts.update({'network': network_data}) design_data = [{ 'power_mode' : equipment['Span']['default'].power_mode, 'span_power_range' : equipment['Span']['default'].delta_power_range_db, 'design_pch' : equipment['SI']['default'].power_dbm, 'baud_rate' : equipment['SI']['default'].baud_rate }] result_dicts.update({'design': design_data}) simulation_data = [] result_dicts.update({'simulation results': simulation_data}) power_mode = equipment['Span']['default'].power_mode print('\n'.join([f'Power mode is set to {power_mode}', f'=> it can be modified in eqpt_config.json - Span'])) pref_ch_db = lin2db(req.power*1e3) #reference channel power / span (SL=20dB) pref_total_db = pref_ch_db + lin2db(req.nb_channel) #reference total power / span (SL=20dB) build_network(network, equipment, pref_ch_db, pref_total_db) path = compute_constrained_path(network, req) if len([s.length for s in path if isinstance(s, RamanFiber)]): if sim_params is None: print(f'{ansi_escapes.red}Invocation error:{ansi_escapes.reset} RamanFiber requires passing simulation params via --sim-params') exit(1) configure_network(network, sim_params) spans = [s.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} and {destination.uid}') print(f'\nNow propagating between {source.uid} and {destination.uid}:') try: p_start, p_stop, p_step = equipment['SI']['default'].power_range_db p_num = abs(int(round((p_stop - p_start)/p_step))) + 1 if p_step != 0 else 1 power_range = list(linspace(p_start, p_stop, p_num)) except TypeError: print('invalid power range definition in eqpt_config, should be power_range_db: [lower, upper, step]') power_range = [0] if not power_mode: #power cannot be changed in gain mode power_range = [0] for dp_db in power_range: req.power = db2lin(pref_ch_db + dp_db)*1e-3 if power_mode: print(f'\nPropagating with input power = {ansi_escapes.cyan}{lin2db(req.power*1e3):.2f} dBm{ansi_escapes.reset}:') else: print(f'\nPropagating in {ansi_escapes.cyan}gain mode{ansi_escapes.reset}: power cannot be set manually') infos = propagate2(path, req, equipment) if len(power_range) == 1: for elem in path: print(elem) if power_mode: print(f'\nTransmission result for input power = {lin2db(req.power*1e3):.2f} dBm:') else: print(f'\nTransmission results:') print(f' Final SNR total (signal bw): {ansi_escapes.cyan}{mean(destination.snr):.02f} dB{ansi_escapes.reset}') #print(f'\n !!!!!!!!!!!!!!!!! TEST POINT !!!!!!!!!!!!!!!!!!!!!') #print(f'carriers ase output of {path[1]} =\n {list(path[1].carriers("out", "nli"))}') # => use "in" or "out" parameter # => use "nli" or "ase" or "signal" or "total" parameter if power_mode: simulation_data.append({ 'Pch_dBm' : pref_ch_db + dp_db, 'OSNR_ASE_0.1nm' : round(mean(destination.osnr_ase_01nm),2), 'OSNR_ASE_signal_bw' : round(mean(destination.osnr_ase),2), 'SNR_nli_signal_bw' : round(mean(destination.osnr_nli),2), 'SNR_total_signal_bw' : round(mean(destination.snr),2) }) else: simulation_data.append({ 'gain_mode' : 'power canot be set', 'OSNR_ASE_0.1nm' : round(mean(destination.osnr_ase_01nm),2), 'OSNR_ASE_signal_bw' : round(mean(destination.osnr_ase),2), 'SNR_nli_signal_bw' : round(mean(destination.osnr_nli),2), 'SNR_total_signal_bw' : round(mean(destination.snr),2) }) write_csv(result_dicts, 'simulation_result.csv') return path, infos
logger.info(f'source = {args.source!r}') logger.info(f'destination = {args.destination!r}') params = {} params['request_id'] = 0 params['trx_type'] = '' params['trx_mode'] = '' params['source'] = source.uid params['destination'] = destination.uid params['nodes_list'] = [destination.uid] params['loose_list'] = ['strict'] params['format'] = '' params['path_bandwidth'] = 0 trx_params = trx_mode_params(equipment) if args.power: trx_params['power'] = db2lin(float(args.power))*1e-3 params.update(trx_params) req = Path_request(**params) path, infos = main(network, equipment, source, destination, sim_params, req) save_network(args.filename, network) if args.show_channels: print('\nThe total SNR per channel at the end of the line is:') print('{:>5}{:>26}{:>26}{:>28}{:>28}{:>28}' \ .format('Ch. #', 'Channel frequency (THz)', 'Channel power (dBm)', 'OSNR ASE (signal bw, dB)', 'SNR NLI (signal bw, dB)', 'SNR total (signal bw, dB)')) for final_carrier, ch_osnr, ch_snr_nl, ch_snr in zip(infos[path[-1]][1].carriers, path[-1].osnr_ase, path[-1].osnr_nli, path[-1].snr): ch_freq = final_carrier.frequency * 1e-12 ch_power = lin2db(final_carrier.power.signal*1e3) print('{:5}{:26.2f}{:26.2f}{:28.2f}{:28.2f}{:28.2f}' \ .format(final_carrier.channel_number, round(ch_freq, 2), round(ch_power, 2), round(ch_osnr, 2), round(ch_snr_nl, 2), round(ch_snr, 2)))
def __init__(self, Request, equipment, bidir): # 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}' # 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.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 else: msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.' # print(msg) logger.critical(msg) 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}\' with mode: \'{Request.mode}\' in eqpt library \nComputation stopped.' # print(msg) logger.critical(msg) raise ServiceError(msg) # excel input are in GHz and dBm if Request.spacing is not None: self.spacing = Request.spacing * 1e9 else: msg = f'Request {self.request_id} missing spacing: spacing is mandatory.\ncomputation stopped' logger.critical(msg) 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 value = correct_xlrd_int_to_str_reading(Request.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(' | ') self.loose = 'LOOSE' if Request.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
def lin_attenuation(self): return db2lin(self.length * self.loss_coef)
def lin_attenuation(self): attenuation = self.length * self.loss_coef return db2lin(attenuation)
def nf_model(amp_dict): gain_min = amp_dict[GAIN_MIN_FIELD] gain_max = amp_dict[GAIN_MAX_FIELD] nf_min = amp_dict.get(NF_MIN_FIELD,-100) nf_max = amp_dict.get(NF_MAX_FIELD,-100) if nf_min<-10 or nf_max<-10: raise ValueError(f'invalid or missing nf_min or nf_max values in eqpt_config.json for {amp_dict["type_variety"]}') nf_min = amp_dict.get(NF_MIN_FIELD,-100) nf_max = amp_dict.get(NF_MAX_FIELD,-100) #use NF estimation model based on NFmin and NFmax in json OA file delta_p = 5 #max power dB difference between 1st and 2nd stage coils #dB g1a = (1st stage gain) - (internal voa attenuation) g1a_min = gain_min - (gain_max-gain_min) - delta_p g1a_max = gain_max - delta_p #nf1 and nf2 are the nf of the 1st and 2nd stage coils #calculate nf1 and nf2 values that solve nf_[min/max] = nf1 + nf2 / g1a[min/max] nf2 = lin2db((db2lin(nf_min) - db2lin(nf_max)) / (1/db2lin(g1a_max)-1/db2lin(g1a_min))) nf1 = lin2db(db2lin(nf_min)- db2lin(nf2)/db2lin(g1a_max)) #expression (1) #now checking and recalculating the results: #recalculate delta_p to check it is within [1-6] boundaries #This is to check that the nf_min and nf_max values from the json file #make sense. If not a warning is printed if nf1 < 4: print('1st coil nf calculated value {} is too low: revise inputs'.format(nf1)) if nf2 < nf1 + 0.3 or nf2 > nf1 + 2: #nf2 should be with [nf1+0.5 - nf1 +2] boundaries #there shouldn't be very high nf differences between 2 coils #=> recalculate delta_p nf2 = max(nf2, nf1+0.3) nf2 = min(nf2, nf1+2) g1a_max = lin2db(db2lin(nf2) / (db2lin(nf_min) - db2lin(nf1))) #use expression (1) delta_p = gain_max - g1a_max g1a_min = gain_min - (gain_max-gain_min) - delta_p if delta_p < 1 or delta_p > 6: #delta_p should be > 1dB and < 6dB => consider user warning if not print('!WARNING!: 1st coil vs 2nd coil calculated DeltaP {} is not valid: revise inputs' .format(delta_p)) #check the calculated values for nf1 & nf2: nf_min_calc = lin2db(db2lin(nf1) + db2lin(nf2)/db2lin(g1a_max)) nf_max_calc = lin2db(db2lin(nf1) + db2lin(nf2)/db2lin(g1a_min)) if (abs(nf_min_calc-nf_min) > 0.01) or (abs(nf_max_calc-nf_max) > 0.01): raise ValueError(f'nf model calculation failed with nf_min {nf_min_calc} and nf_max {nf_max_calc} calculated') return nf1, nf2, delta_p
def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=False): """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 #if called from transmission_main.py, trx_mode is '' if trx_mode is not None: mode_params = next(mode for trx in trxs \ if trx == trx_type_variety \ for mode in trxs[trx].mode \ if mode['format'] == trx_mode) trx_params = {**mode_params} # sanity check: spacing baudrate must be smaller than min spacing if trx_params['baud_rate'] > trx_params['min_spacing']: msg = f'Inconsistency in equipment library:\n Transpoder "{trx_type_variety}" mode "{trx_params["format"]}" '+\ f'has baud rate: {trx_params["baud_rate"]*1e-9} GHz greater than min_spacing {trx_params["min_spacing"]*1e-9}.' print(msg) exit() 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} trx_params['f_min'] = equipment['Transceiver'][ trx_type_variety].frequency['min'] trx_params['f_max'] = equipment['Transceiver'][ trx_type_variety].frequency['max'] # TODO: novel automatic feature maybe unwanted if spacing is specified # trx_params['spacing'] = automatic_spacing(trx_params['baud_rate']) # temp = trx_params['spacing'] # print(f'spacing {temp}') except StopIteration: if error_message: print( f'could not find tsp : {trx_type_variety} with mode: {trx_mode} in eqpt library' ) print('Computation stopped.') exit() else: # default transponder charcteristics # mainly used with transmission_main_example.py trx_params['f_min'] = default_si_data.f_min trx_params['f_max'] = default_si_data.f_max trx_params['baud_rate'] = default_si_data.baud_rate trx_params['spacing'] = default_si_data.spacing trx_params['OSNR'] = None trx_params['bit_rate'] = None trx_params['cost'] = None trx_params['roll_off'] = default_si_data.roll_off trx_params['tx_osnr'] = default_si_data.tx_osnr trx_params['min_spacing'] = None 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 test_roadm_target_power(prev_node_type, effective_pch_out_db, power_dbm): ''' Check that egress power of roadm is equal to target power if input power is greater than target power else, that it is equal to input power. Use a simple two hops A-B-C topology for the test where the prev_node in ROADM B is either an amplifier or a fused, so that the target power can not be met in this last case. ''' equipment = load_equipment(EQPT_LIBRARY_NAME) json_network = load_json(TEST_DIR / 'data/twohops_roadm_power_test.json') prev_node = next(n for n in json_network['elements'] if n['uid'] == 'west edfa in node B to ila2') json_network['elements'].remove(prev_node) if prev_node_type == 'edfa': prev_node = {'uid': 'west edfa in node B to ila2', 'type': 'Edfa'} elif prev_node_type == 'fused': prev_node = {'uid': 'west edfa in node B to ila2', 'type': 'Fused'} prev_node['params'] = {'loss': 0} json_network['elements'].append(prev_node) network = network_from_json(json_network, equipment) p_total_db = power_dbm + lin2db( automatic_nch(equipment['SI']['default'].f_min, equipment['SI']['default'].f_max, equipment['SI']['default'].spacing)) build_network(network, equipment, power_dbm, p_total_db) params = { 'request_id': 0, 'trx_type': '', 'trx_mode': '', 'source': 'trx node A', 'destination': 'trx node C', 'bidir': False, 'nodes_list': ['trx node C'], 'loose_list': ['strict'], 'format': '', 'path_bandwidth': 100e9, 'effective_freq_slot': None, } trx_params = trx_mode_params(equipment) params.update(trx_params) req = PathRequest(**params) req.power = db2lin(power_dbm - 30) path = compute_constrained_path(network, req) 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): carriers_power_in_roadm = min([ c.power.signal + c.power.nli + c.power.ase for c in si.carriers ]) si = el(si, degree=path[i + 1].uid) if el.uid == 'roadm node B': print('input', carriers_power_in_roadm) # if previous was an EDFA, power level at ROADM input is enough for the ROADM to apply its # target power (as specified in equipment ie -20 dBm) # if it is a Fused, the input power to the ROADM is smaller than the target power, and the # ROADM cannot apply this target. In this case, it is assumed that the ROADM has 0 dB loss # so the output power will be the same as the input power, which for this particular case # corresponds to -22dBm + power_dbm # next step (for ROADM modelling) will be to apply a minimum loss for ROADMs ! if prev_node_type == 'edfa': assert el.effective_pch_out_db == effective_pch_out_db if prev_node_type == 'fused': # then output power == input_power == effective_pch_out_db + power_dbm assert effective_pch_out_db + power_dbm == \ pytest.approx(lin2db(carriers_power_in_roadm * 1e3), rel=1e-3) assert el.effective_pch_out_db == effective_pch_out_db + power_dbm for carrier in si.carriers: print(carrier.power.signal + carrier.power.nli + carrier.power.ase) power = carrier.power.signal + carrier.power.nli + carrier.power.ase if prev_node_type == 'edfa': # edfa prev_node sets input power to roadm to a high enough value: # Check that egress power of roadm is equal to target power assert power == pytest.approx( db2lin(effective_pch_out_db - 30), rel=1e-3) elif prev_node_type == 'fused': # fused prev_node does reamplfy power after fiber propagation, so input power # to roadm is low. # Check that egress power of roadm is equalized to the min carrier input power. assert power == pytest.approx(carriers_power_in_roadm, rel=1e-3) else: si = el(si)