def parse_headers(my_sheet, input_headers_dict, headers, start_line, slice_in): """return a dict of header_slice key = column index value = header name""" for h0 in input_headers_dict: slice_out = read_slice(my_sheet, start_line, slice_in, h0) iteration = 1 while slice_out == (-1, -1) and iteration < 10: # try next lines slice_out = read_slice(my_sheet, start_line + iteration, slice_in, h0) iteration += 1 if slice_out == (-1, -1): if h0 in ('east', 'Node A', 'Node Z', 'City'): print( f'{ansi_escapes.red}CRITICAL{ansi_escapes.reset}: missing _{h0}_ header: EXECUTION ENDS' ) raise NetworkTopologyError(f'Missing _{h0}_ header') else: print(f'missing header {h0}') elif not isinstance(input_headers_dict[h0], dict): headers[slice_out[0]] = input_headers_dict[h0] else: headers = parse_headers(my_sheet, input_headers_dict[h0], headers, start_line + 1, slice_out) if headers == {}: print( f'{ansi_escapes.red}CRITICAL ERROR{ansi_escapes.reset}: could not find any header to read _ ABORT' ) raise NetworkTopologyError('Could not find any header to read') return headers
def sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city): duplicate_links = [] for l1 in links: for l2 in links: if l1 is not l2 and l1 == l2 and l2 not in duplicate_links: print(f'\nWARNING\n \ link {l1.from_city}-{l1.to_city} is duplicate \ \nthe 1st duplicate link will be removed but you should check Links sheet input' ) duplicate_links.append(l1) for l in duplicate_links: links.remove(l) unreferenced_nodes = [n for n in nodes_by_city if n not in links_by_city] if unreferenced_nodes: raise NetworkTopologyError( f'{ansi_escapes.red}XLS error:{ansi_escapes.reset} The following nodes are not ' f'referenced from the {ansi_escapes.cyan}Links{ansi_escapes.reset} sheet. ' f'If unused, remove them from the {ansi_escapes.cyan}Nodes{ansi_escapes.reset} ' f'sheet:\n' + _format_items(unreferenced_nodes)) # no need to check "Links" for invalid nodes because that's already in parse_excel() wrong_eqpt_from = [n for n in eqpts_by_city if n not in nodes_by_city] wrong_eqpt_to = [ n.to_city for destinations in eqpts_by_city.values() for n in destinations if n.to_city not in nodes_by_city ] wrong_eqpt = wrong_eqpt_from + wrong_eqpt_to if wrong_eqpt: raise NetworkTopologyError( f'{ansi_escapes.red}XLS error:{ansi_escapes.reset} ' f'The {ansi_escapes.cyan}Eqpt{ansi_escapes.reset} sheet refers to nodes that ' f'are not defined in the {ansi_escapes.cyan}Nodes{ansi_escapes.reset} sheet:\n' + _format_items(wrong_eqpt)) for city, link in links_by_city.items(): if nodes_by_city[city].node_type.lower() == 'ila' and len(link) != 2: # wrong input: ILA sites can only be Degree 2 # => correct to make it a ROADM and remove entry in links_by_city # TODO: put in log rather than print print(f'invalid node type ({nodes_by_city[city].node_type})\ specified in {city}, replaced by ROADM') nodes_by_city[city].node_type = 'ROADM' for n in nodes: if n.city == city: n.node_type = 'ROADM' return nodes, links
def add_connector_loss(network, fibers, default_con_in, default_con_out, EOL): for fiber in fibers: try: next_node = next(network.successors(fiber)) except StopIteration: raise NetworkTopologyError(f'Fiber {fiber.uid} is not properly connected, please check network topology') if fiber.params.con_in is None: fiber.params.con_in = default_con_in if fiber.params.con_out is None: fiber.params.con_out = default_con_out if not isinstance(next_node, elements.Fused): fiber.params.con_out += EOL
def prev_node_generator(network, node): """fused spans interest: iterate over all predecessors while they are either Fused or Fibers succeeded by Fused""" try: prev_node = next(network.predecessors(node)) except StopIteration: if isinstance(node, elements.Transceiver): return raise NetworkTopologyError(f'Node {node.uid} is not properly connected, please check network topology') if ((isinstance(prev_node, elements.Fused) and isinstance(node, _fiber_fused_types)) or (isinstance(prev_node, _fiber_fused_types) and isinstance(node, elements.Fused))): yield prev_node yield from prev_node_generator(network, prev_node)
def next_node_generator(network, node): """fused spans interest: iterate over all successors while they are Fused or Fiber type""" try: next_node = next(network.successors(node)) except StopIteration: raise NetworkTopologyError('Node {node.uid} is not properly connected, please check network topology') # yield and re-iterate if isinstance(next_node, elements.Fused) or isinstance(node, elements.Fused): yield next_node yield from next_node_generator(network, next_node) else: StopIteration
def split_fiber(network, fiber, bounds, target_length, equipment): new_length, n_spans = calculate_new_length(fiber.length, bounds, target_length) if n_spans == 1: return try: next_node = next(network.successors(fiber)) prev_node = next(network.predecessors(fiber)) except StopIteration: raise NetworkTopologyError( f'Fiber {fiber.uid} is not properly connected, please check network topology' ) network.remove_node(fiber) fiber_params = fiber.params._asdict() fiber_params['length'] = new_length / UNITS[fiber.params.length_units] fiber_params['con_in'] = fiber.con_in fiber_params['con_out'] = fiber.con_out f = interp1d([prev_node.lng, next_node.lng], [prev_node.lat, next_node.lat]) xpos = [ prev_node.lng + (next_node.lng - prev_node.lng) * (n + 1) / (n_spans + 1) for n in range(n_spans) ] ypos = f(xpos) for span, lng, lat in zip(range(n_spans), xpos, ypos): new_span = Fiber(uid=f'{fiber.uid}_({span+1}/{n_spans})', metadata={ 'location': { 'latitude': lat, 'longitude': lng, 'city': fiber.loc.city, 'region': fiber.loc.region, } }, params=fiber_params) if isinstance(prev_node, Fiber): edgeweight = prev_node.params.length else: edgeweight = 0.01 network.add_edge(prev_node, new_span, weight=edgeweight) prev_node = new_span if isinstance(prev_node, Fiber): edgeweight = prev_node.params.length else: edgeweight = 0.01 network.add_edge(prev_node, next_node, weight=edgeweight)
def split_fiber(network, fiber, bounds, target_length, equipment): new_length, n_spans = calculate_new_length(fiber.params.length, bounds, target_length) if n_spans == 1: return try: next_node = next(network.successors(fiber)) prev_node = next(network.predecessors(fiber)) except StopIteration: raise NetworkTopologyError( f'Fiber {fiber.uid} is not properly connected, please check network topology' ) network.remove_node(fiber) fiber.params.length = new_length xpos = [ prev_node.lng + (next_node.lng - prev_node.lng) * (n + 0.5) / n_spans for n in range(n_spans) ] ypos = [ prev_node.lat + (next_node.lat - prev_node.lat) * (n + 0.5) / n_spans for n in range(n_spans) ] for span, lng, lat in zip(range(n_spans), xpos, ypos): new_span = elements.Fiber(uid=f'{fiber.uid}_({span+1}/{n_spans})', type_variety=fiber.type_variety, metadata={ 'location': { 'latitude': lat, 'longitude': lng, 'city': fiber.loc.city, 'region': fiber.loc.region, } }, params=fiber.params.asdict()) if isinstance(prev_node, elements.Fiber): edgeweight = prev_node.params.length else: edgeweight = 0.01 network.add_edge(prev_node, new_span, weight=edgeweight) prev_node = new_span if isinstance(prev_node, elements.Fiber): edgeweight = prev_node.params.length else: edgeweight = 0.01 network.add_edge(prev_node, next_node, weight=edgeweight)
def prev_node_generator(network, node): """fused spans interest: iterate over all predecessors while they are Fused or Fiber type""" try: prev_node = next(n for n in network.predecessors(node)) except StopIteration: raise NetworkTopologyError( f'Node {node.uid} is not properly connected, please check network topology' ) # yield and re-iterate if isinstance(prev_node, Fused) or isinstance(node, Fused): yield prev_node yield from prev_node_generator(network, prev_node) else: StopIteration
def network_from_json(json_data, equipment): # NOTE|dutc: we could use the following, but it would tie our data format # too closely to the graph library # from networkx import node_link_graph g = DiGraph() for el_config in json_data['elements']: typ = el_config.pop('type') variety = el_config.pop('type_variety', 'default') cls = _cls_for(typ) if typ == 'Fused': # well, there's no variety for the 'Fused' node type pass elif variety in equipment[typ]: extra_params = equipment[typ][variety] temp = el_config.setdefault('params', {}) temp = merge_amplifier_restrictions(temp, extra_params.__dict__) el_config['params'] = temp el_config['type_variety'] = variety elif (typ in ['Fiber', 'RamanFiber' ]) or (typ == 'Edfa' and variety not in ['default', '']): raise ConfigurationError( f'The {typ} of variety type {variety} was not recognized:' '\nplease check it is properly defined in the eqpt_config json file' ) el = cls(**el_config) g.add_node(el) nodes = {k.uid: k for k in g.nodes()} for cx in json_data['connections']: from_node, to_node = cx['from_node'], cx['to_node'] try: if isinstance(nodes[from_node], elements.Fiber): edge_length = nodes[from_node].params.length else: edge_length = 0.01 g.add_edge(nodes[from_node], nodes[to_node], weight=edge_length) except KeyError: raise NetworkTopologyError( f'can not find {from_node} or {to_node} defined in {cx}') return g
def sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city): duplicate_links = [] for l1 in links: for l2 in links: if l1 is not l2 and l1 == l2 and l2 not in duplicate_links: print(f'\nWARNING\n \ link {l1.from_city}-{l1.to_city} is duplicate \ \nthe 1st duplicate link will be removed but you should check Links sheet input' ) duplicate_links.append(l1) for l in duplicate_links: links.remove(l) try: test_nodes = [n for n in nodes_by_city if n not in links_by_city] test_links = [n for n in links_by_city if n not in nodes_by_city] test_eqpts = [n for n in eqpts_by_city if n not in nodes_by_city] assert (test_nodes == [] or test_nodes == [''])\ and (test_links == [] or test_links == [''])\ and (test_eqpts == [] or test_eqpts == ['']) except AssertionError: msg = f'CRITICAL error in excel input: Names in Nodes and Links sheets do no match, check:\ \n{test_nodes} in Nodes sheet\ \n{test_links} in Links sheet\ \n{test_eqpts} in Eqpt sheet' raise NetworkTopologyError(msg) for city, link in links_by_city.items(): if nodes_by_city[city].node_type.lower() == 'ila' and len(link) != 2: # wrong input: ILA sites can only be Degree 2 # => correct to make it a ROADM and remove entry in links_by_city # TODO: put in log rather than print print(f'invalid node type ({nodes_by_city[city].node_type})\ specified in {city}, replaced by ROADM') nodes_by_city[city].node_type = 'ROADM' for n in nodes: if n.city == city: n.node_type = 'ROADM' return nodes, links
def add_fiber_padding(network, fibers, padding): """last_fibers = (fiber for n in network.nodes() if not (isinstance(n, elements.Fiber) or isinstance(n, elements.Fused)) for fiber in network.predecessors(n) if isinstance(fiber, elements.Fiber))""" for fiber in fibers: try: next_node = next(network.successors(fiber)) except StopIteration: raise NetworkTopologyError(f'Fiber {fiber.uid} is not properly connected, please check network topology') if isinstance(next_node, elements.Fused): continue this_span_loss = span_loss(network, fiber) if this_span_loss < padding: # add a padding att_in at the input of the 1st fiber: # address the case when several fibers are spliced together first_fiber = find_first_node(network, fiber) # in order to support no booster , fused might be placed # just after a roadm: need to check that first_fiber is really a fiber if isinstance(first_fiber, elements.Fiber): first_fiber.params.att_in = first_fiber.params.att_in + padding - this_span_loss
def parse_excel(input_filename): link_headers = { 'Node A': 'from_city', 'Node Z': 'to_city', 'east': { 'Distance (km)': 'east_distance', 'Fiber type': 'east_fiber', 'lineic att': 'east_lineic', 'Con_in': 'east_con_in', 'Con_out': 'east_con_out', 'PMD': 'east_pmd', 'Cable id': 'east_cable' }, 'west': { 'Distance (km)': 'west_distance', 'Fiber type': 'west_fiber', 'lineic att': 'west_lineic', 'Con_in': 'west_con_in', 'Con_out': 'west_con_out', 'PMD': 'west_pmd', 'Cable id': 'west_cable' } } node_headers = { 'City': 'city', 'State': 'state', 'Country': 'country', 'Region': 'region', 'Latitude': 'latitude', 'Longitude': 'longitude', 'Type': 'node_type', 'Booster_restriction': 'booster_restriction', 'Preamp_restriction': 'preamp_restriction' } eqpt_headers = { 'Node A': 'from_city', 'Node Z': 'to_city', 'east': { 'amp type': 'east_amp_type', 'att_in': 'east_att_in', 'amp gain': 'east_amp_gain', 'delta p': 'east_amp_dp', 'tilt': 'east_tilt', 'att_out': 'east_att_out' }, 'west': { 'amp type': 'west_amp_type', 'att_in': 'west_att_in', 'amp gain': 'west_amp_gain', 'delta p': 'west_amp_dp', 'tilt': 'west_tilt', 'att_out': 'west_att_out' } } roadm_headers = { 'Node A': 'from_node', 'Node Z': 'to_node', 'per degree target power (dBm)': 'target_pch_out_db' } with open_workbook(input_filename) as wb: nodes_sheet = wb.sheet_by_name('Nodes') links_sheet = wb.sheet_by_name('Links') try: eqpt_sheet = wb.sheet_by_name('Eqpt') except Exception: # eqpt_sheet is optional eqpt_sheet = None try: roadm_sheet = wb.sheet_by_name('Roadms') except Exception: # roadm_sheet is optional roadm_sheet = None nodes = [] for node in parse_sheet(nodes_sheet, node_headers, NODES_LINE, NODES_LINE + 1, NODES_COLUMN): nodes.append(Node(**node)) expected_node_types = {'ROADM', 'ILA', 'FUSED'} for n in nodes: if n.node_type not in expected_node_types: n.node_type = 'ILA' links = [] for link in parse_sheet(links_sheet, link_headers, LINKS_LINE, LINKS_LINE + 2, LINKS_COLUMN): links.append(Link(**link)) eqpts = [] if eqpt_sheet is not None: for eqpt in parse_sheet(eqpt_sheet, eqpt_headers, EQPTS_LINE, EQPTS_LINE + 2, EQPTS_COLUMN): eqpts.append(Eqpt(**eqpt)) roadms = [] if roadm_sheet is not None: for roadm in parse_sheet(roadm_sheet, roadm_headers, ROADMS_LINE, ROADMS_LINE + 2, ROADMS_COLUMN): roadms.append(Roadm(**roadm)) # sanity check all_cities = Counter(n.city for n in nodes) if len(all_cities) != len(nodes): raise ValueError(f'Duplicate city: {all_cities}') bad_links = [] for lnk in links: if lnk.from_city not in all_cities or lnk.to_city not in all_cities: bad_links.append([lnk.from_city, lnk.to_city]) if bad_links: raise NetworkTopologyError( f'{ansi_escapes.red}XLS error:{ansi_escapes.reset} ' f'The {ansi_escapes.cyan}Links{ansi_escapes.reset} sheet references nodes that ' f'are not defined in the {ansi_escapes.cyan}Nodes{ansi_escapes.reset} sheet:\n' + _format_items(f'{item[0]} -> {item[1]}' for item in bad_links)) return nodes, links, eqpts, roadms
def set_egress_amplifier(network, this_node, equipment, pref_ch_db, pref_total_db): """ this node can be a transceiver or a ROADM (same function called in both cases) """ power_mode = equipment['Span']['default'].power_mode next_oms = (n for n in network.successors(this_node) if not isinstance(n, elements.Transceiver)) this_node_degree = { k: v for k, v in this_node.per_degree_pch_out_db.items() } if hasattr(this_node, 'per_degree_pch_out_db') else {} for oms in next_oms: # go through all the OMS departing from the ROADM prev_node = this_node node = oms # if isinstance(next_node, elements.Fused): #support ROADM wo egress amp for metro applications # node = find_last_node(next_node) # next_node = next(n for n in network.successors(node)) # next_node = find_last_node(next_node) if node.uid not in this_node_degree: # if no target power is defined on this degree or no per degree target power is given use the global one # if target_pch_out_db is not an attribute, then the element must be a transceiver this_node_degree[node.uid] = getattr(this_node.params, 'target_pch_out_db', 0) # use the target power on this degree prev_dp = this_node_degree[node.uid] - pref_ch_db dp = prev_dp prev_voa = 0 voa = 0 visited_nodes = [] while not (isinstance(node, elements.Roadm) or isinstance(node, elements.Transceiver)): # go through all nodes in the OMS (loop until next Roadm instance) try: next_node = next(network.successors(node)) except StopIteration: raise NetworkTopologyError( f'{type(node).__name__} {node.uid} is not properly connected, please check network topology' ) visited_nodes.append(node) if next_node in visited_nodes: raise NetworkTopologyError( f'Loop detected for {type(node).__name__} {node.uid}, please check network topology' ) if isinstance(node, elements.Edfa): node_loss = span_loss(network, prev_node) voa = node.out_voa if node.out_voa else 0 if node.delta_p is None: dp = target_power(network, next_node, equipment) + voa else: dp = node.delta_p if node.effective_gain is None or power_mode: gain_target = node_loss + dp - prev_dp + prev_voa else: # gain mode with effective_gain gain_target = node.effective_gain dp = prev_dp - node_loss - prev_voa + gain_target power_target = pref_total_db + dp if isinstance(prev_node, elements.Fiber): max_fiber_lineic_loss_for_raman = \ equipment['Span']['default'].max_fiber_lineic_loss_for_raman * 1e-3 # dB/m raman_allowed = prev_node.params.loss_coef < max_fiber_lineic_loss_for_raman else: raman_allowed = False if node.params.type_variety == '': if node.variety_list and isinstance( node.variety_list, list): restrictions = node.variety_list elif isinstance( prev_node, elements.Roadm ) and prev_node.restrictions['booster_variety_list']: # implementation of restrictions on roadm boosters restrictions = prev_node.restrictions[ 'booster_variety_list'] elif isinstance( next_node, elements.Roadm ) and next_node.restrictions['preamp_variety_list']: # implementation of restrictions on roadm preamp restrictions = next_node.restrictions[ 'preamp_variety_list'] else: restrictions = None edfa_variety, power_reduction = select_edfa( raman_allowed, gain_target, power_target, equipment, node.uid, restrictions) extra_params = equipment['Edfa'][edfa_variety] node.params.update_params(extra_params.__dict__) dp += power_reduction gain_target += power_reduction else: if node.params.raman and not raman_allowed: if isinstance(prev_node, elements.Fiber): print( f'{ansi_escapes.red}WARNING{ansi_escapes.reset}: raman is used in node {node.uid}\n ' 'but fiber lineic loss is above threshold\n') else: print( f'{ansi_escapes.red}WARNING{ansi_escapes.reset}: raman is used in node {node.uid}\n ' 'but previous node is not a fiber\n') # if variety is imposed by user, and if the gain_target (computed or imposed) is also above # variety max gain + extended range, then warn that gain > max_gain + extended range if gain_target - equipment['Edfa'][node.params.type_variety].gain_flatmax - \ equipment['Span']['default'].target_extended_gain > 1e-2: # 1e-2 to allow a small margin according to round2float min step print( f'{ansi_escapes.red}WARNING{ansi_escapes.reset}: ' f'WARNING: effective gain in Node {node.uid} is above user ' f'specified amplifier {node.params.type_variety}\n' f'max flat gain: {equipment["Edfa"][node.params.type_variety].gain_flatmax}dB ; ' f'required gain: {gain_target}dB. Please check amplifier type.' ) node.delta_p = dp if power_mode else None node.effective_gain = gain_target set_amplifier_voa(node, power_target, power_mode) prev_dp = dp prev_voa = voa prev_node = node node = next_node # print(f'{node.uid}') if isinstance(this_node, elements.Roadm): this_node.per_degree_pch_out_db = { k: v for k, v in this_node_degree.items() }
def set_egress_amplifier(network, this_node, equipment, pref_ch_db, pref_total_db): """ this node can be a transceiver or a ROADM (same function called in both cases) """ power_mode = equipment['Span']['default'].power_mode next_oms = (n for n in network.successors(this_node) if not isinstance(n, elements.Transceiver)) this_node_degree = { k: v for k, v in this_node.per_degree_pch_out_db.items() } if hasattr(this_node, 'per_degree_pch_out_db') else {} for oms in next_oms: # go through all the OMS departing from the ROADM prev_node = this_node node = oms # if isinstance(next_node, elements.Fused): #support ROADM wo egress amp for metro applications # node = find_last_node(next_node) # next_node = next(n for n in network.successors(node)) # next_node = find_last_node(next_node) if this_node_degree: # find the target power on this degree if node.uid in this_node_degree.keys(): prev_dp = this_node_degree[node.uid] - pref_ch_db else: # if no target power is defined on this degree use the global one # if target_pch_out_db is not an attribute, then the element must be a transceiver prev_dp = getattr(this_node.params, 'target_pch_out_db', 0) - pref_ch_db this_node_degree[node.uid] = prev_dp else: # if no per degree target power is given use the global one # if target_pch_out_db is not an attribute, then the element must be a transceiver prev_dp = getattr(this_node.params, 'target_pch_out_db', 0) - pref_ch_db this_node_degree[node.uid] = prev_dp dp = prev_dp prev_voa = 0 voa = 0 visited_nodes = [] while not (isinstance(node, elements.Roadm) or isinstance(node, elements.Transceiver)): # go through all nodes in the OMS (loop until next Roadm instance) try: next_node = next(network.successors(node)) except StopIteration: raise NetworkTopologyError( f'{type(node).__name__} {node.uid} is not properly connected, please check network topology' ) visited_nodes.append(node) if next_node in visited_nodes: raise NetworkTopologyError( f'Loop detected for {type(node).__name__} {node.uid}, please check network topology' ) if isinstance(node, elements.Edfa): node_loss = span_loss(network, prev_node) voa = node.out_voa if node.out_voa else 0 if node.delta_p is None: dp = target_power(network, next_node, equipment) else: dp = node.delta_p if node.effective_gain is None or power_mode: gain_target = node_loss + dp - prev_dp + prev_voa else: # gain mode with effective_gain gain_target = node.effective_gain dp = prev_dp - node_loss - prev_voa + gain_target power_target = pref_total_db + dp if isinstance(prev_node, elements.Fiber): max_fiber_lineic_loss_for_raman = \ equipment['Span']['default'].max_fiber_lineic_loss_for_raman raman_allowed = prev_node.params.loss_coef < max_fiber_lineic_loss_for_raman else: raman_allowed = False # implementation of restrictions on roadm boosters if isinstance(prev_node, elements.Roadm): if prev_node.restrictions['booster_variety_list']: restrictions = prev_node.restrictions[ 'booster_variety_list'] else: restrictions = None elif isinstance(next_node, elements.Roadm): # implementation of restrictions on roadm preamp if next_node.restrictions['preamp_variety_list']: restrictions = next_node.restrictions[ 'preamp_variety_list'] else: restrictions = None else: restrictions = None if node.params.type_variety == '': edfa_variety, power_reduction = select_edfa( raman_allowed, gain_target, power_target, equipment, node.uid, restrictions) extra_params = equipment['Edfa'][edfa_variety] node.params.update_params(extra_params.__dict__) dp += power_reduction gain_target += power_reduction elif node.params.raman and not raman_allowed: print( f'{ansi_escapes.red}WARNING{ansi_escapes.reset}: raman is used in node {node.uid}\n but fiber lineic loss is above threshold\n' ) node.delta_p = dp if power_mode else None node.effective_gain = gain_target set_amplifier_voa(node, power_target, power_mode) prev_dp = dp prev_voa = voa prev_node = node node = next_node # print(f'{node.uid}') if isinstance(this_node, elements.Roadm): this_node.per_degree_pch_out_db = { k: v for k, v in this_node_degree.items() }
def parse_excel(input_filename): link_headers = { 'Node A': 'from_city', 'Node Z': 'to_city', 'east': { 'Distance (km)': 'east_distance', 'Fiber type': 'east_fiber', 'lineic att': 'east_lineic', 'Con_in': 'east_con_in', 'Con_out': 'east_con_out', 'PMD': 'east_pmd', 'Cable id': 'east_cable' }, 'west': { 'Distance (km)': 'west_distance', 'Fiber type': 'west_fiber', 'lineic att': 'west_lineic', 'Con_in': 'west_con_in', 'Con_out': 'west_con_out', 'PMD': 'west_pmd', 'Cable id': 'west_cable' } } node_headers = { 'City': 'city', 'State': 'state', 'Country': 'country', 'Region': 'region', 'Latitude': 'latitude', 'Longitude': 'longitude', 'Type': 'node_type', 'Booster_restriction': 'booster_restriction', 'Preamp_restriction': 'preamp_restriction' } eqpt_headers = { 'Node A': 'from_city', 'Node Z': 'to_city', 'east': { 'amp type': 'east_amp_type', 'att_in': 'east_att_in', 'amp gain': 'east_amp_gain', 'delta p': 'east_amp_dp', 'tilt': 'east_tilt', 'att_out': 'east_att_out' }, 'west': { 'amp type': 'west_amp_type', 'att_in': 'west_att_in', 'amp gain': 'west_amp_gain', 'delta p': 'west_amp_dp', 'tilt': 'west_tilt', 'att_out': 'west_att_out' } } with open_workbook(input_filename) as wb: nodes_sheet = wb.sheet_by_name('Nodes') links_sheet = wb.sheet_by_name('Links') try: eqpt_sheet = wb.sheet_by_name('Eqpt') except Exception: # eqpt_sheet is optional eqpt_sheet = None nodes = [] for node in parse_sheet(nodes_sheet, node_headers, NODES_LINE, NODES_LINE + 1, NODES_COLUMN): nodes.append(Node(**node)) expected_node_types = {'ROADM', 'ILA', 'FUSED'} for n in nodes: if n.node_type not in expected_node_types: n.node_type = 'ILA' links = [] for link in parse_sheet(links_sheet, link_headers, LINKS_LINE, LINKS_LINE + 2, LINKS_COLUMN): links.append(Link(**link)) eqpts = [] if eqpt_sheet is not None: for eqpt in parse_sheet(eqpt_sheet, eqpt_headers, EQPTS_LINE, EQPTS_LINE + 2, EQPTS_COLUMN): eqpts.append(Eqpt(**eqpt)) # sanity check all_cities = Counter(n.city for n in nodes) if len(all_cities) != len(nodes): raise ValueError(f'Duplicate city: {all_cities}') bad_links = [] for lnk in links: if lnk.from_city not in all_cities or lnk.to_city not in all_cities: bad_links.append([lnk.from_city, lnk.to_city]) if bad_links: raise NetworkTopologyError(f'Bad link(s): {bad_links}.') return nodes, links, eqpts