diff --git a/mnoptical/dataplane.py b/mnoptical/dataplane.py index c45ee4c3..55c6635c 100755 --- a/mnoptical/dataplane.py +++ b/mnoptical/dataplane.py @@ -338,7 +338,7 @@ def configTx( self, txNum, channel=None, power=None ): if channel is not None: self.txChannel[ txNum ] = channel if power is not None: - transceiver.operation_power = db_to_abs(power) + self.model.tx_config(transceiver, operational_power_dBm=power) def txnum( self, wdmPort ): "Return a tx number for wdmPort number" diff --git a/mnoptical/link.py b/mnoptical/link.py index f5ae532d..04af4167 100644 --- a/mnoptical/link.py +++ b/mnoptical/link.py @@ -43,12 +43,17 @@ def __init__(self, src_node, dst_node, src_out_port=-1, dst_in_port=-1, if boost_amp: self.boost_amp.set_input_port(self.src_node, self, input_port=0) self.boost_amp.set_output_port(spans[0][0], self, output_port=0) + if self.boost_amp.prev_component: + raise Exception(f"{self.boost_amp} already connected to " + f"{self.boost_amp.prev_component}") self.boost_amp.prev_component = src_node self.boost_amp.next_component = spans[0][0] prev_amp = None for i, span in enumerate(spans): prev_span = span[0] + if getattr(prev_span, 'link', None) is not None: + raise Exception(f"span {prev_span} is already used in {prev_span.link}") prev_span.link = self amplifier = span[1] @@ -58,6 +63,10 @@ def __init__(self, src_node, dst_node, src_out_port=-1, dst_in_port=-1, prev_span.prev_component = prev_amp if amplifier: + if amplifier.link: + raise Exception( + f"{amplifier} is already used in {amplifier.link}") + amplifier.link = self amplifier.set_input_port(prev_span, self, input_port=0) @@ -91,6 +100,8 @@ def describe(self): def __repr__(self): """String representation""" + dport = self.dst_node.link_to_port_in[self] + was: "(%s->%s:%s)" % (self.src_node, self.dst_node, dport) return "(%s->%s)" % (self.src_node, self.dst_node) def reset(self): @@ -159,15 +170,17 @@ def propagate(self, is_last_port=False, safe_switch=False): if self.boost_amp: for optical_signal in self.optical_signals: # associate boost_amp to optical signal at input interface - self.boost_amp.include_optical_signal_in(optical_signal, - in_port=0) + state = optical_signal.loc_in_to_state[self] + self.boost_amp.include_optical_signal_in( + optical_signal, **state, in_port=0) self.boost_amp.propagate(self.optical_signals, is_last_port=is_last_port, safe_switch=safe_switch) else: first_span = self.spans[0][0] for optical_signal in self.optical_signals: - first_span.include_optical_signal_in(optical_signal) + state = optical_signal.loc_in_to_state[self] + first_span.include_optical_signal_in(optical_signal, **state) first_span.propagate(is_last_port=is_last_port, safe_switch=safe_switch) @@ -294,7 +307,7 @@ def propagate(self, is_last_port=False, safe_switch=False): nli_noise=optical_signal.loc_out_to_state[self]['nli_noise'], in_port=in_port) self.next_component.receiver(optical_signal, in_port) - elif isinstance(self.next_component, Roadm): + elif hasattr(self.next_component, 'include_optical_signal_in_roadm'): self.next_component.include_optical_signal_in_roadm( optical_signal, power=optical_signal.loc_out_to_state[self]['power'], @@ -308,10 +321,12 @@ def propagate(self, is_last_port=False, safe_switch=False): ase_noise=optical_signal.loc_out_to_state[self]['ase_noise'], nli_noise=optical_signal.loc_out_to_state[self]['nli_noise'], in_port=0) + else: + print(f"{self} NOT PROPAGATING SIGNAL") if isinstance(self.next_component, Amplifier): self.next_component.propagate(self.optical_signals, is_last_port=is_last_port, safe_switch=safe_switch) - elif isinstance(self.next_component, Roadm) and is_last_port: + elif hasattr(self.next_component, 'switch') and is_last_port: in_port = self.next_component.link_to_port_in[self.link] self.next_component.switch(in_port, self.link.src_node, safe_switch=safe_switch) diff --git a/mnoptical/network.py b/mnoptical/network.py index 8ae6f420..932ceefe 100644 --- a/mnoptical/network.py +++ b/mnoptical/network.py @@ -18,59 +18,56 @@ def __init__(self): self.name_to_node = {} - def add_lt(self, name, transceivers=None, **params): + def add_node(self, name, cls, *args, nodes=None, **params): + """ + Add node to network + :param name: name of node + :param cls: node class/constructor + :param nodes: list that node will be appended to + """ + if nodes is None: nodes = [] + if name in self.name_to_node: + raise ValueError(f"Network.add_node: node {name} already exists!") + node = cls(name, *args, **params) + self.name_to_node[name] = node + nodes.append(node) + self.topology[node] = [] + return node + + def add_lt(self, name, *args, cls=LineTerminal, **params): """ Add lt node :param name: name of lt :param transceivers: transceivers of LT for automated instantiation + :param cls: optional LineTerminal class/constructor :return: added lt """ - if name in self.name_to_node: - raise ValueError("Network.add_lt: lt with this name already exist!") - configs = {'name': name, - 'transceivers': transceivers} - configs.update(params) - lt = LineTerminal(**configs) - self.name_to_node[name] = lt - self.line_terminals.append(lt) - self.topology[lt] = [] - return lt + return self.add_node(name, cls, *args, nodes=self.line_terminals, + **params) - def add_roadm(self, name, **params): + def add_roadm(self, name, *args, cls=Roadm, **params): """ Add a ROADM node. :param name: name of ROADM + :cls: optional Roadm class/constructor :return: added ROADM """ - if name in self.name_to_node: - raise ValueError("Network.add_roadm: ROADM with this name already exist!!") - configs = {'name': name} - configs.update(params) - roadm = Roadm(**configs) - self.name_to_node[name] = roadm - self.roadms.append(roadm) - self.topology[roadm] = [] - return roadm + return self.add_node(name, cls, *args, nodes=self.roadms, + **params) - def add_amplifier(self, name, amplifier_type='EDFA', **params): + def add_amplifier(self, name, *args, cls=Amplifier, **params): """ Add an Amplifier node. :param name: name of Amplifier :param amplifier_type: amplifier type (currently supporting only EDFA) + :param cls: optional Amplifier class/constructor :return: added Amplifier """ - if name in self.name_to_node: - raise ValueError("Network.add_amplifier: Amplifier with this name already exist!! %s" % str(name)) - configs = {'name': name, - 'amplifier_type': amplifier_type} - configs.update(params) - amplifier = Amplifier(**configs) - self.name_to_node[name] = amplifier - self.amplifiers.append(amplifier) - return amplifier + return self.add_node(name, cls, *args, nodes=self.amplifiers, + **params) def add_link(self, src_node, dst_node, src_out_port=-1, - dst_in_port=-1, boost_amp=None, spans=None): + dst_in_port=-1, boost_amp=None, spans=None, cls=Link): """ Add a uni-directional link :param src_node: source node in link @@ -79,14 +76,14 @@ def add_link(self, src_node, dst_node, src_out_port=-1, :param dst_in_port: dst_node input port :param boost_amp: optional amplifier object for boost_amplification :param spans: + :param cls: optional Link class/constructor :return: created and added link """ - link = Link(src_node, dst_node, - src_out_port=src_out_port, - dst_in_port=dst_in_port, - boost_amp=boost_amp, - spans=spans) - + link = cls(src_node, dst_node, + src_out_port=src_out_port, + dst_in_port=dst_in_port, + boost_amp=boost_amp, + spans=spans) self.links.append(link) self.topology[src_node].append((dst_node, link)) return link diff --git a/mnoptical/node.py b/mnoptical/node.py index 1c4ac480..02c3fd5d 100755 --- a/mnoptical/node.py +++ b/mnoptical/node.py @@ -123,7 +123,8 @@ def remove_optical_signal(self, optical_signal): for out_port, optical_signals in port_to_optical_signal_out_copy.items(): if optical_signal in optical_signals: self.port_to_optical_signal_out[out_port].remove(optical_signal) - if not isinstance(self, Amplifier): + # Was: not isinstance(self, Amplifier) + if not hasattr(self, 'target_gain'): link = self.port_to_link_out[out_port] link.remove_optical_signal(optical_signal) @@ -285,18 +286,16 @@ def assoc_tx_to_channel(self, transceiver, channel, out_port=-1): def assoc_channel(self, transceiver, channel, out_port): # instantiate OpticalSignal object + power = transceiver.operation_power optical_signal = OpticalSignal(channel, transceiver.channel_spacing_H, transceiver.channel_spacing_nm, transceiver.modulation_format, transceiver.symbol_rate, - transceiver.bits_per_symbol, - power=transceiver.operation_power) - + transceiver.bits_per_symbol, power=power) # associate transceiver to optical_signal transceiver.assoc_optical_signal(optical_signal) - dst_node = self.port_to_node_out[out_port] # the goal of this function - self.include_optical_signal_out(optical_signal, out_port=out_port) + self.include_optical_signal_out(optical_signal, power=power, out_port=out_port) self.tx_to_channel[out_port] = {'optical_signal': optical_signal, 'transceiver': transceiver} self.optical_signals_out += 1 @@ -349,15 +348,18 @@ def turn_on(self, safe_switch=False): for signal_count, out_port in enumerate(self.tx_to_channel, start=1): optical_signal = self.tx_to_channel[out_port]['optical_signal'] transceiver = self.tx_to_channel[out_port]['transceiver'] - transceiver.optical_signal.reset(component=self) + transceiver.optical_signal.reset() + # Originate signal here + power = transceiver.operation_power + self.include_optical_signal_out(optical_signal, power=power) if self.debugger: print("*** %s.turn_on %s on port %s" % (self, optical_signal, out_port)) - # pass signal info to link + # Pass signal info to link + # Note: power is always in watts link = self.port_to_link_out[out_port] - link.include_optical_signal_in(optical_signal) - + link.include_optical_signal_in(optical_signal, power=power) if signal_count == self.optical_signals_out: link.propagate(is_last_port=True, safe_switch=safe_switch) else: @@ -535,22 +537,19 @@ def assoc_loc_in(self, loc, power=None, ase_noise=None, nli_noise=None): Associate a location to signal performance values at the input interface of this point :param loc: location (i.e., node, span) - :param power: power levels [mW] (or None for default/launch state) - :param ase_noise: ase levels [mW] (or None for default/launch state) - :param nli_noise: nli levels [mW] (or None for default/launch state) - """ - if power is None: - power = self.power - if ase_noise is None: - ase_noise = self.ase_noise - if nli_noise is None: - nli_noise = self.nli_noise - # XXX: We probably shouldn't update the default/launch state to - # some random input state in the network, should we? - self.power = power - self.ase_noise = ase_noise - self.nli_noise = nli_noise - self.loc_in_to_state[loc] = {'power': power, 'ase_noise': ase_noise, 'nli_noise': nli_noise} + :param power: power levels [mW] + :param ase_noise: ase levels [mW] + :param nli_noise: nli levels [mW] + """ + assert (power, ase_noise, nli_noise) != (None, None, None) + state = self.loc_in_to_state.setdefault( + loc, dict(power=0, ase_noise=0, nli_noise=0)) + if power is not None: + state['power'] = power + if ase_noise is not None: + state['ase_noise'] = ase_noise + if nli_noise is not None: + state['nli_noise'] = nli_noise def assoc_loc_out(self, loc, power=None, ase_noise=None, nli_noise=None): """ @@ -561,18 +560,15 @@ def assoc_loc_out(self, loc, power=None, ase_noise=None, nli_noise=None): :param ase_noise: ase levels [mW] (or None for default/launch state) :param nli_noise: nli levels [mW] (or None for default/launch state) """ - if power is None: - power = self.power - if ase_noise is None: - ase_noise = self.ase_noise - if nli_noise is None: - nli_noise = self.nli_noise - # XXX: We probably shouldn't update the default/launch state to - # some random input state in the network, should we? - self.power = power - self.ase_noise = ase_noise - self.nli_noise = nli_noise - self.loc_out_to_state[loc] = {'power': power, 'ase_noise': ase_noise, 'nli_noise': nli_noise} + assert (power, ase_noise, nli_noise) != (None, None, None) + state = self.loc_out_to_state.setdefault( + loc, dict(power=0, ase_noise=0, nli_noise=0)) + if power is not None: + state['power'] = power + if ase_noise is not None: + state['ase_noise'] = ase_noise + if nli_noise is not None: + state['nli_noise'] = nli_noise def reset(self, component=None): """ @@ -584,7 +580,7 @@ def reset(self, component=None): self.ase_noise = self.ase_noise_start self.nli_noise = self.nli_noise_start # Reset signal state at all components, optionally - # presrving state at originating component + # preserving state at originating component self.loc_in_to_state = {} if component and component in self.loc_out_to_state: self.loc_out_to_state = {component: self.loc_out_to_state[component]} @@ -868,6 +864,7 @@ def can_switch(self, in_port, safe_switch): port_out_to_port_in_signals = {} # iterate through the optical signals that are currently at # in_port (if any) + self.port_to_optical_signal_in.setdefault(in_port, []) for optical_signal in self.port_to_optical_signal_in[in_port]: # check if there is a switching rule for a signal switch_rule = False @@ -887,7 +884,8 @@ def can_switch(self, in_port, safe_switch): port_out_to_port_in_signals[out_port][in_port].append(optical_signal) if not switch_rule: if self.debugger: - print(self, "Unable to find switch rule for signal:", optical_signal) + print(self, "Unable to find switch rule for signal:", + optical_signal, "input port:", in_port) port_to_optical_signal_out_copy = port_to_optical_signal_out.copy() # Check if there are other signals being switched at these output ports. @@ -897,7 +895,7 @@ def can_switch(self, in_port, safe_switch): # used by the switching rules found for the input port. for out_port, optical_signals in port_to_optical_signal_out_copy.items(): # iterate through all signals at an output port - for optical_signal in self.port_to_optical_signal_out[out_port]: + for optical_signal in self.port_to_optical_signal_out.get(out_port, []): # add the adjecent signals to the dictionaries if optical_signal not in optical_signals: port_to_optical_signal_out[out_port].append(optical_signal) @@ -913,6 +911,7 @@ def can_switch(self, in_port, safe_switch): if len(port_to_optical_signal_out) > 0: # iterate through the output ports for out_port, optical_signals in port_to_optical_signal_out_copy.items(): + if out_port not in self.port_to_optical_signal_out: continue # check if optical_signals == self.port_to_optical_signal_out[out_port] # check if the power levels have changed if all(optical_signal in optical_signals for optical_signal in @@ -921,8 +920,8 @@ def can_switch(self, in_port, safe_switch): self.port_check_range_out[out_port] += 1 if not self.power_divergence(optical_signals, in_port): if self.port_check_range_out[out_port] > self.check_range_th: - # these signals can be safely terminated at a LineTerminal - if not isinstance(self.port_to_node_out[out_port], LineTerminal): + # these signals can be safely received at a LineTerminal + if not hasattr(self.port_to_node_out[out_port], 'receiver'): if self.debugger: print('RoadmWarning:', self, "same signals already propagated on this output " "port. Stopping propagation.") @@ -983,7 +982,8 @@ def switch(self, in_port, src_node, safe_switch=False): Note: check for switch feasibility unless performing tasks independent of switching (i.e., EDFA gain configuration). """ - if isinstance(src_node, LineTerminal): + # Was: isinstance(src_node, LineTerminal) + if hasattr(src_node, 'transceivers'): # need to check for all (possible) input ports coming from LineTerminal port_to_optical_signal_out, port_out_to_port_in_signals = self.can_switch_from_lt(src_node, safe_switch) else: @@ -1369,30 +1369,33 @@ def propagate(self, optical_signals, is_last_port=False, safe_switch=False): ase_noise_out = optical_signal.loc_out_to_state[self]['ase_noise'] nli_noise_out = optical_signal.loc_out_to_state[self]['nli_noise'] - if isinstance(self.next_component, LineTerminal): + # LineTerminal requires us to call receiver() + if hasattr(self.next_component, 'receiver'): in_port = self.next_component.link_to_port_in[self.link] self.next_component.include_optical_signal_in( optical_signal, power=power_out, ase_noise=ase_noise_out, nli_noise=nli_noise_out, in_port=in_port) self.next_component.receiver(optical_signal, in_port) - elif isinstance(self.next_component, Roadm): + # Roadm has its own flavor of include_optical_signal_in() + elif hasattr(self.next_component, 'include_optical_signal_in_roadm'): in_port = self.next_component.link_to_port_in[self.link] self.next_component.include_optical_signal_in_roadm( optical_signal, power=power_out, ase_noise=ase_noise_out, nli_noise=nli_noise_out, in_port=in_port) + # Otherwise do the normal thing else: self.next_component.include_optical_signal_in( optical_signal, power=power_out, ase_noise=ase_noise_out, nli_noise=nli_noise_out) - # Trigger the action for the next component + # Trigger the action for the next component if needed if self.next_component: - if self.next_component.__class__.__name__ == 'Span': - self.next_component.propagate(is_last_port=is_last_port, safe_switch=safe_switch) - elif isinstance(self.next_component, Roadm): + if hasattr(self.next_component, 'switch'): self.next_component.switch(in_port, self.link.src_node, safe_switch=safe_switch) + elif hasattr(self.next_component,'propagate'): + self.next_component.propagate(is_last_port=is_last_port, safe_switch=safe_switch) def set_gain(self, gain_dB): """ @@ -1416,6 +1419,59 @@ def mock_amp_gain_adjust(self, new_gain): self.target_gain = new_gain self.system_gain = new_gain +class Splitter(Node): + """ + Simple static splitter, for now without coupling loss. + Input at port 0 is split among output ports according + to the split array, e.g. {1: 99, 2:1} indicating + a 99%/1% split for output ports 1 and 2. + + There is some flexibility since the percents do not + need to add up to 100, though they should not exceed + 100 since this is really not supposed to be an amplifier! + + There is currently no error checking. + + We use the same ingress API/protocol as Roadm. + """ + + def __init__(self, name, split=None, monitor_mode='out'): + "split: {port:percent...}" + super().__init__(name) + self.split = split or {} + self.monitor = Monitor(name + "-monitor", component=self, mode=monitor_mode) + + def switch(self, in_port, src_node, safe_switch=False): + """Propagate splitter's signals to its output ports. + This is part of the Roadm protocol that we conform to.""" + siglists = self.port_to_optical_signal_in.values() + assert len(siglists) == 1, f"{self}: a Splitter can only have one input port" + signals = tuple(siglists)[0] + # Compute output signals + for signal in signals: + state = signal.loc_in_to_state[self] + power_in, ase_in, nli_in = state['power'], state['ase_noise'], state['nli_noise'] + for port in self.ports_out: + fraction = self.split.get(port, 0.0) / 100.0 + power_out = power_in * fraction + ase_out = ase_in * fraction + nli_out = nli_in * fraction + self.include_optical_signal_out( + signal, power=power_out, ase_noise=ase_out, nli_noise=nli_out, out_port=port) + link = self.port_to_link_out.get(port, None) + if link: + link.include_optical_signal_in( + signal, power=power_out, ase_noise=ase_out, nli_noise=nli_out) + # Propagate to output links + for port, link in self.port_to_link_out.items(): + link.propagate(is_last_port=True, safe_switch=safe_switch) + + def include_optical_signal_in_roadm(self, optical_signal, power=None, ase_noise=None, + nli_noise=None, in_port=0): + "Here to signal that we are conforming to the Roadm protocol" + self.include_optical_signal_in( + optical_signal, power=power, ase_noise=ase_noise, nli_noise=nli_noise, in_port=in_port) + class Monitor(Node): """ diff --git a/mnoptical/ofcdemo/Simulation_API.py b/mnoptical/ofcdemo/Simulation_API.py index 80c1aa0e..d2419ae4 100644 --- a/mnoptical/ofcdemo/Simulation_API.py +++ b/mnoptical/ofcdemo/Simulation_API.py @@ -27,14 +27,14 @@ def ROADM_voaPowerLeveling(self, node, outport, power, channel): "Power control for a signal channel at a roadm using VOA leveling" #print('leveling power, port, channel:', power, outport, channel) + # FIXME: this seems to be missing from the code and the units are suspect node.configure_voa(channel_id=channel, output_port=outport, operational_power_dB=power) - def Terminal_configChannelPower(self, terminal, channel, power): - "Congifure Terminal Launch power for a channel" - - terminal.transceivers[channel-1].operation_power = db_to_abs(power) - #terminal.name_to_transceivers['tx%d' % channel].operation_power = db_to_abs(power) + def Terminal_configChannelPower(self, terminal, channel, power_dBm): + "Configure Terminal Launch power (in dBm) for a channel" + terminal.tx_config(transceivers[channel-1], operational_power_dBm=power_dBm) + #terminal.name_to_transceivers['tx%d' % channel].operation_power = db_to_abs(power_dBm)*1e-3 def Terminal_configChannel(self, terminal, channel): diff --git a/mnoptical/ofcdemo/demo_2021.py b/mnoptical/ofcdemo/demo_2021.py index 4a64526e..2f302226 100644 --- a/mnoptical/ofcdemo/demo_2021.py +++ b/mnoptical/ofcdemo/demo_2021.py @@ -13,7 +13,7 @@ from mininet.clean import cleanup from mininet.node import RemoteController -from sys import argv +from sys import argv, stdout from os.path import dirname from subprocess import check_call @@ -32,5 +32,7 @@ check_call("python3 -m mnoptical.ofcdemo.Demo_Control_2".split()) else: CLI( net ) + stdout.flush() restServer.stop() + stdout.flush() net.stop() diff --git a/mnoptical/rest.py b/mnoptical/rest.py index 8b478829..d71b4e97 100755 --- a/mnoptical/rest.py +++ b/mnoptical/rest.py @@ -7,6 +7,7 @@ from wsgiref.simple_server import make_server, WSGIRequestHandler from bottle import route, get, post, request, default_app, abort from threading import Thread +from sys import stdout from mnoptical.dataplane import SwitchBase, Terminal, ROADM, OpticalLink from mininet.node import Switch @@ -276,4 +277,5 @@ def start( self ): def stop( self ): self.server.shutdown() + stdout.flush() self.thread.join() diff --git a/tests/splittertest.py b/tests/splittertest.py new file mode 100755 index 00000000..fb36a2f6 --- /dev/null +++ b/tests/splittertest.py @@ -0,0 +1,166 @@ +#!/usr/bin/python3 +""" +splittertest.py: test splitter element + +We create a 99/1 power split and verify that it +is working as expected. +""" + +from mnoptical.network import Network +# Note: Segment() is a (Fiber(), Amplifier()) tuple +from mnoptical.link import Span as Fiber, SpanTuple as Segment +from mnoptical.node import Transceiver, Roadm, LineTerminal, Splitter, Amplifier +from mnoptical.units import abs_to_db + +# Units +km = dB = dBm = 1.0 +m = .001 + +# Terminal TX/RX port numbers (arbitrary for simulation) +TX, RX = 100, 200 + +# Splitter input and output ports +IN0, OUT1, OUT2 = 0, 1, 2 + +# Roadm line in/out and add/drop base +LINEIN, LINEOUT, ADD, DROP = 0, 1, RX, TX + +# Number of transceivers +txcount = 3 + +# Network topology +def createnetwork(): + """Simple test network t1 -> r1 -> splitter(99/1) -> r3 -> t3 + -> r2 -> t2 """ + net = Network() + + def span(length, amp='', **params): + "Span helper function" + if amp: amp = net.add_amplifier(ampname, **params) + return Segment(span=Fiber(length=length), amplifier=amp) + + # Terminals and Roadms + for i in 1, 2, 3: + transceivers = [Transceiver(i,f'tx{i}',0*dBm) + for i in range(1, txcount+1)] + net.add_lt(f't{i}', transceivers=transceivers, + monitor_mode='in') + net.add_roadm(f'r{i}') + t1, t2, t3 = net.line_terminals + r1, r2, r3 = net.roadms + + # 99/1 Splitter + sp1 = net.add_node('sp1', cls=Splitter, split={OUT1:99, OUT2:1}) + + # Local uplinks and downlinks + L = net.add_link + for ch in range(1, txcount+1): + L(t1, r1, spans=[span(1*m)], + src_out_port=TX+ch, dst_in_port=ADD+ch) + L(r2, t2, spans=[span(1*m)], + src_out_port=DROP+ch, dst_in_port=RX+ch) + L(r3, t3, spans=[span(1*m)], + src_out_port=DROP+ch, dst_in_port=RX+ch) + + # Long link before splitter + net.add_link( + r1, sp1, boost_amp=net.add_amplifier('boost1', target_gain=0.0), + spans=[span(25*km)], src_out_port=LINEOUT, dst_in_port=IN0) + + # Short links from splitter + net.add_link(sp1, r2, spans=[span(1*m)], + src_out_port=OUT1, dst_in_port=LINEIN) + net.add_link(sp1, r3, spans=[span(1*m)], + src_out_port=OUT2, dst_in_port=LINEIN) + + return net + +# Monitoring helper functions +def getsignalwatts(node, port=None): + "Return monitored signal, ase noise, and nli noise power in watts" + monitor = node.monitor + return {s.index: {'pwrW': monitor.get_power(s), + 'aseW': monitor.get_ase_noise(s), + 'nliW': monitor.get_nli_noise(s)} + for s in monitor.get_optical_signals(port)} + +def wtodbm(W): + "Return watts as dBm" + return abs_to_db(W*1000.0) if W != 0 else float('-inf') + +def printdbm(sigwatts, fn=wtodbm, units='dBm'): + "Print signal watts as dBm" + for ch, entries in sigwatts.items(): + pdbm = fn(entries['pwrW']) + adbm = fn(entries['aseW']) + ndbm = fn(entries['nliW']) + print(f'ch{ch}: pwr {pdbm:.2e}{units} ' + f'ase {adbm:.2e}{units} nli {ndbm:.2e}{units}') + +def printmw(sigwatts): + "Print signal watts as watts" + printdbm(sigwatts, fn=lambda W: W*1e3, units='mW') + +# Configuration + +def configroadms(net): + "Configure ROADMs (as mux or demux)" + r1, r2, r3 = net.roadms + for ch in range(1, txcount+1): + r1.install_switch_rule(ADD+ch, LINEOUT, ch) + r2.install_switch_rule(LINEIN, DROP+ch, ch) + r3.install_switch_rule(LINEIN, DROP+ch, ch) + +def configterms(net): + "Configure terminals" + t1, t2, t3 = net.line_terminals + # Set TX and RX channels + for t in range(1, txcount+1): + ch = t # channel number == transceiver number + tx1 = t1.id_to_transceivers[t] + rx2 = t2.id_to_transceivers[t] + rx3 = t3.id_to_transceivers[t] + t1.assoc_tx_to_channel(tx1, ch, out_port=TX+ch) + t2.assoc_rx_to_channel(rx2, ch, in_port=RX+ch) + t3.assoc_rx_to_channel(rx3, ch, in_port=RX+ch) + # Start transmission + for term in t1, t2, t3: + term.turn_on() + +def ratio(a, b): + "Return percent ratio of a to b" + total = a+b + apct = int(100.0 * a/total + .5) + bpct = int(100.0 * b/total + .5) + return apct, bpct + +def test(): + "Verify that a 99/1 split works as expected" + # Create and configure network + net = createnetwork() + configroadms(net) + configterms(net) + # Check power + t1, t2, t3 = net.line_terminals + print(f"*** {t2} power") + t2watts = getsignalwatts(t2) + printmw(t2watts) + print(f"*** {t3} power") + t3watts = getsignalwatts(t3) + printmw(t3watts) + print(f"*** Checking split ratios") + errors = 0 + for ch in range(1,4): + for f in 'pwr', 'ase': + w = f'{f}W' + a, b = ratio(t2watts[ch][w], t3watts[ch][w]) + print(f"ch{ch} {f} split is {a}/{b}") + if (a,b) != (99,1): + print(f'Expected 99/1 split but got {a}/{b}') + print(f"*** Completed with {errors} errors.") + return errors + +if __name__ == '__main__': + result = test() + exit(result) +