Source code for opics.utils

from typing import Any, Dict, List, Tuple
import cmath as cm
import time
import re
import itertools
import inspect
from copy import deepcopy
import numpy as np
from numpy import ndarray
from pathlib import PosixPath
from defusedxml.ElementTree import parse


[docs]def fromSI(value: str) -> float: """converts from SI unit values to metric Args: value (str): a value in SI units, e.g. 1.3u Returns: float: the value in metric units. """ return float(value.replace("u", "e-6"))
[docs]def universal_sparam_filereader( nports: int, sfilename: str, sfiledir: PosixPath, format_type: str = "auto" ) -> Tuple[ndarray, ndarray]: """ Function to automatically detect the sparameter file format and use appropriate method to delimit and format sparam data This function is a unified version of sparameter reader function defined in https://github.com/BYUCamachoLab/simphony Args: nports: Number of ports sfilename: XML look-up-table filename sfiledir: Path to the directory containing the XML file format_type: Format type. For more information: https://support.lumerical.com/hc/en-us/articles/360036618513-S-parameter-file-formats """ numports = nports filename = sfiledir / sfilename if format_type == "auto": try: # print("try A") result = universal_sparam_filereader(nports, sfilename, sfiledir, "A") return result except Exception: try: # print("try B") result = universal_sparam_filereader(nports, sfilename, sfiledir, "B") return result except Exception: # print("try C") result = universal_sparam_filereader(nports, sfilename, sfiledir, "C") return result elif format_type == "A": """ dc_halfring_te_1550 Returns the s-parameters across some frequency range for the Sparam fileformat A input: ["port 1",""] ["port 2",""] ["port 3",""] ["port 4",""] ("port 1","mode 1",1,"port 1",1,"transmission") (101, 3) output: [frequency, s-parameters] """ F = [] S = [] with open(filename, "r") as fid: for i in range(5): line = fid.readline() line = fid.readline() numrows = int(tuple(line[1:-2].split(","))[0]) S = np.zeros((numrows, numports, numports), dtype="complex128") r = m = n = 0 for line in fid: if line[0] == "(": continue data = line.split() data = list(map(float, data)) if m == 0 and n == 0: F.append(data[0]) S[r, m, n] = data[1] * np.exp(1j * data[2]) r += 1 if r == numrows: r = 0 m += 1 if m == numports: m = 0 n += 1 if n == numports: break return (np.array(F), S) elif format_type == "B": """ ebeam_bdc_te1550, nanotaper, ebeam_y_1550 Returns the s-parameters across some frequency range for the Sparam fileformat A input: ('port 1','TE',1,'port 1',1,'transmission') (51,3) output: [frequency, s-parameters] """ F = [] S = [] with open(filename, "r") as fid: line = fid.readline() line = fid.readline() numrows = int(tuple(line[1:-2].split(","))[0]) S = np.zeros((numrows, numports, numports), dtype="complex128") r = m = n = 0 for line in fid: if line[0] == "(": continue data = line.split() data = list(map(float, data)) if m == 0 and n == 0: F.append(data[0]) S[r, m, n] = data[1] * np.exp(1j * data[2]) r += 1 if r == numrows: r = 0 m += 1 if m == numports: m = 0 n += 1 if n == numports: break return (np.array(F), S) elif format_type == "C": """ ebeam_gc_te1550 Returns the s-parameters across some frequency range for the Sparam fileformat A input: columns with space delimiter output: [frequency, s-parameters] """ with open(filename) as fid: # grating coupler compact models have 100 points for each s-matrix index arrlen = 100 lines = fid.readlines() F = np.zeros(arrlen) S = np.zeros((arrlen, 2, 2), "complex128") for i in range(0, arrlen): words = lines[i].split() F[i] = float(words[0]) S[i, 0, 0] = cm.rect(float(words[1]), float(words[2])) S[i, 0, 1] = cm.rect(float(words[3]), float(words[4])) S[i, 1, 0] = cm.rect(float(words[5]), float(words[6])) S[i, 1, 1] = cm.rect(float(words[7]), float(words[8])) F = F[::-1] S = S[::-1, :, :] return (np.array(F), S)
[docs]def LUT_reader(filedir: PosixPath, lutfilename: str, lutdata: List[List[str]]): """ Reads look up table data. Args: filedir: Directory of the XML look-up-table file. lutfilename: Look-up-table filename. lutdata: Look-up-table arguments. """ xml = parse(filedir / lutfilename) root = xml.getroot() for node in root.iter("association"): sample = [[each.attrib["name"], each.text] for each in node.iter("value")] if sorted(sample[0:-1]) == sorted(lutdata): break sparam_file = sample[-1][1].split(";") return (sparam_file, xml, node)
[docs]def LUT_processor( filedir: PosixPath, lutfilename: str, lutdata: List[List[str]], nports: int, sparam_attr: str, verbose: bool = False, ) -> Tuple[Tuple[ndarray, ndarray], str]: """process look up table data""" start = time.time() sparam_file, xml, node = LUT_reader(filedir, lutfilename, lutdata) # read data if ".npz" in sparam_file[0] or ".npz" in sparam_file[-1]: npzfile = [each for each in sparam_file if ".npz" in each][0] tempdata = np.load(filedir / npzfile) sdata = (tempdata["f"], tempdata["s"]) npz_file = npzfile else: if verbose: print("numpy datafile not found. reading sparam file instead..") sdata = universal_sparam_filereader(nports, sparam_file[-1], filedir, "auto") # create npz file name npz_file = sparam_file[-1].split(".")[0] # save as npz file np.savez(filedir / npz_file, f=sdata[0], s=sdata[1]) # update xml file sparam_file.append(npz_file + ".npz") sparam_file = list(set(sparam_file)) for each in node.iter("value"): if each.attrib["name"] == sparam_attr: each.text = ";".join(sparam_file) xml.write(filedir / lutfilename) if verbose: print("SParam data extracted in ", time.time() - start) return (sdata, npz_file)
[docs]def NetlistProcessor(spice_filepath, Network, libraries, c_, circuitData, verbose=True): """ Processes a spice netlist to setup and simulate a circuit. Args: spice_filepath: Path to the spice netlist file. Network: """ if verbose: for key, value in circuitData.items(): print(key, str(value)) # define frequency range and resolution freq = np.linspace( c_ / circuitData["sim_params"][0], c_ / circuitData["sim_params"][1], circuitData["sim_params"][2], ) # create a circuit subckt = Network(network_id=circuitData["networkID"], f=freq) # get library all_libraries = dict( [ each for each in inspect.getmembers(libraries, inspect.ismodule) if each[0][0] != "_" ] ) libs_comps = {} for each_lib in list(set(circuitData["compLibs"])): # temp_comps = dict(inspect.getmembers(all_libraries[each_lib], inspect.isclass)) libs_comps[each_lib] = all_libraries[each_lib].component_factory # add circuit components for i in range(len(circuitData["compModels"])): # get component model comp_model = libs_comps[circuitData["compLibs"][i]][ circuitData["compModels"][i] ] # clean attributes cls_attrs = deepcopy(comp_model.cls_attrs) # class attributes comp_attrs = circuitData["compAttrs"][i] # component attributes # clean up attributes for each_attrs in cls_attrs.keys(): if each_attrs in comp_attrs.keys(): cls_attrs[each_attrs] = fromSI(comp_attrs[each_attrs]) subckt.add_component( libs_comps[circuitData["compLibs"][i]][circuitData["compModels"][i]], params=cls_attrs, component_id=circuitData["compLabels"][i], ) # add circuit netlist subckt.global_netlist = circuitData["circuitNets"] # add unique net component connections subckt.current_connections = circuitData["circuitConns"] return subckt
[docs]class netlistParser: "A netlist parser to read spi files generated by SiEPIC tools" def __init__(self, mainfile_path: PosixPath) -> None: self.circuitComponents = [] self.circuitConnections = [] self.mainfile_path = mainfile_path
[docs] def readfile(self) -> Dict[str, Any]: filepath = self.mainfile_path circuitID = "" inp = "" out = "" inp_net = 0 out_net = [] circuitLabels = [] circuitModels = [] circuitConns = [] circuitNets = [] componentLibs = [] componentAttrs = [] component_locations = [] temp_file = open(filepath, "r") temp_lines = temp_file.readlines() free_node_idx = -1 freq_data = [] seek_component = 0 seek_ona = 0 orthogonal_ID = 0 # extract circuit connectivity for each_line in temp_lines: each_line = re.sub(" +", " ", each_line.strip()) # remove empty lines if each_line.startswith("*"): continue else: each_line = "".join( [ "".join(filter(None, each_section.split(" "))) if ('"' in each_section) else each_section for each_section in re.split( r"""("[^"]*"|'[^']*')""", each_line ) ] ) temp_data = each_line.split(" ") if len(temp_data) > 1: # if line is not an empty one MC_location = [] if temp_data[0] == ".subckt": circuitID = temp_data[1] inp = temp_data[2] out = [temp_data[x] for x in range(3, len(temp_data))] seek_component = 1 elif temp_data[0] == ".param": continue elif temp_data[0] == ".ends": seek_component = 0 elif temp_data[0] == ".ona": seek_ona = 1 elif seek_ona == 1: # ONA related data if len(temp_data) < 3: temp_data = [0] + temp_data[-1].split("=") if temp_data[1] == "orthogonal_identifier": orthogonal_ID = int(temp_data[-1]) elif temp_data[1] == "start": freq_data.append(float(temp_data[-1])) elif temp_data[1] == "stop": freq_data.append(float(temp_data[-1])) elif temp_data[1] == "number_of_points": freq_data.append(int(temp_data[-1])) elif seek_component == 1: # otherwise its component data circuitLabels.append(temp_data[0]) temp_ports = [] found_ports = 0 found_library = 0 for i in range(1, len(temp_data)): # if its an optical port if ( "N$" in temp_data[i] and "N$None".lower() != temp_data[i].lower() ): temp_ports.append(int(temp_data[i].replace("N$", ""))) found_ports = 1 elif "N$None".lower() == temp_data[i].lower(): temp_ports.append(free_node_idx) free_node_idx -= 1 found_ports = 1 elif inp == temp_data[i]: temp_ports.append(free_node_idx) inp_net = free_node_idx free_node_idx -= 1 found_ports = 1 elif out[0] == temp_data[i]: temp_ports.append(free_node_idx) out_net.append(free_node_idx) free_node_idx -= 1 if len(out) > 1: out.pop(0) if len(out) == 0: found_ports = 1 elif found_ports == 1 and "N$" not in temp_data[i]: circuitModels.append(temp_data[i]) temp_cls_atrr = ( {} ) # deepcopy(lib[temp_data[i]].cls_attrs) found_ports = -1 elif "lay" in temp_data[i] or "sch" in temp_data[i]: if "lay" in temp_data[i]: MC_location.append( fromSI(temp_data[i].split("=")[-1]) * 1e6 ) # ignore layout and schematic position data for now. # adapt opics models to accept this data # they are component parameters elif "library" in temp_data[i]: # cprint(temp_data[i]) temp_lib = ( temp_data[i].replace('"', "").split("=")[1].split() ) componentLibs.append( temp_lib[-1].split("/")[-1].lower() ) found_library = 1 elif "=" in temp_data[i] and found_library == 1: # if its a components' attribute temp_attr = temp_data[i].split("=") # print(temp_attr[0]) # if(temp_attr[0] in temp_cls_atrr): temp_cls_atrr[temp_attr[0]] = temp_attr[1].strip('"') componentAttrs.append(temp_cls_atrr) circuitNets.append(temp_ports) if bool(MC_location): component_locations.append(MC_location) circuitConns = list(set(list(itertools.chain(*circuitNets)))) # remove IOs from component connections' list circuitConns = [each for each in circuitConns if each >= 0] # return all data return { "circuitNets": circuitNets, "circuitConns": circuitConns, "compLibs": componentLibs, "compModels": circuitModels, "compLabels": circuitLabels, "compAttrs": componentAttrs, "compLocs": component_locations, "networkID": circuitID, "inp_net": inp_net, "out_net": out_net, "sim_params": freq_data, "OID": orthogonal_ID, }