diff --git a/simphony/formatters.py b/simphony/formatters.py index a4f5769d..be1b6fc5 100644 --- a/simphony/formatters.py +++ b/simphony/formatters.py @@ -67,6 +67,7 @@ def _to_component( pins: List[str], s_params: Optional[np.ndarray] = None, subcircuit: Optional[str] = None, + polar_interpolation: bool = False, ) -> "Model": """Returns a component that is defined by the given parameters. @@ -100,7 +101,7 @@ class StaticModel(Model): def s_parameters(self, _freqs: np.array) -> np.ndarray: try: - return interpolate(_freqs, freqs, s_params) + return interpolate(_freqs, freqs, s_params, polar_interpolation) except ValueError: raise ValueError( f"Frequencies must be between {freqs.min(), freqs.max()}." @@ -191,6 +192,119 @@ def parse(self, string: str) -> "Model": ) +class ModelLumericalFormatter(ModelFormatter): + """The ModelLumericalFormatter class formats the model data in a format compatible with Lumerical.""" + + def parse(self, file_name: str, mode_id: int = 1, name: str = None) -> "Model": + # create name for the model + if name is None: + name = file_name.split(".")[0] + # read the lumerical s-parameters file + file_r = open(file_name, 'r') + string = file_r.read() + file_r.close() + # read the header information + header = string[:string.index("(") - 1] + body = string[string.index("(") + 2:] + pin_list = header.split("\n") + pins = [] + for pin_title in pin_list: + pin_details = pin_title.split("\"") + pins.append(pin_details[1]) + # read the body information + body_sections = body.split("(\"") + freq_num = len(body_sections[0].strip().split("\n")) - 2 + s_params = np.zeros([freq_num, len(pins), len(pins)], dtype=complex) + freqs = [] + record_freqs = True + # for each input port and output port + for body_section in body_sections: + connection_header = body_section[:body_section.index(")")] + connection_info = connection_header.split(",") + # read which ports the following s-parameters apply to + in_pin = connection_info[0][:len(connection_info[0]) - 1] + out_pin = connection_info[3][1:len(connection_info[3]) - 1] + connection_meas = body_section[body_section.index(")") + 2:] + connection_data = connection_meas[connection_meas.index(")") + 2:] + # read the s-parameters + connection_data_points = connection_data.split("\n") + if(int(connection_info[2]) != mode_id): + continue + else: + # for each frequency + for point in connection_data_points[:len(connection_data_points) - 1]: + point_info = point.strip().split(" ") + freq = float(point_info[0]) + mag = float(point_info[1]) + angle = float(point_info[2]) + s_param = complex(mag * np.cos(angle), mag * np.sin(angle)) + if(record_freqs): + freqs.append(freq) + # add the s-parameter to the correct location in the s-parameter matrix + s_params[freqs.index(freq), pins.index(in_pin), pins.index(out_pin)] = s_param + record_freqs = False + # return the component + return self._to_component( + np.array(freqs), + name, + pins, + s_params, + None, + polar_interpolation=True, + ) + + def format(self, component: "Model", freqs: np.array, orientations=None, file_name: str = None) -> None: + # get the information from the component + name, pins, s_params, subcircuit = self._from_component(component, freqs) + mode_name = "mode 1" + mode_id = 1 + # create the file name + if file_name is None: + file_name = name + ".txt" + lum_string = "" + # generate the port orienations if not inputted + if(orientations is None): + orientations = [] + switch_point = np.ceil(len(pins) / 2.0) + for i in range(len(pins)): + if(i < switch_point): + orientations.append("LEFT") + else: + orientations.append("RIGHT") + # convert the s-parameters to magnitude and phase format + mags = np.abs(s_params) + angles = np.arctan2(s_params.imag, s_params.real) + angles = np.unwrap(angles, axis=0) + # write the header information + for pin, orientation in zip(pins, orientations): + lum_string = lum_string + "[\"" + pin + "\",\"" + orientation + "\"]\n" + input_pin_count = 0 + # write the body information + for input_pin in pins: + output_pin_count = 0 + for output_pin in pins: + # write the input and output port information + lum_string = lum_string + "(\"" + input_pin + "\",\"" + mode_name + "\"," + str(mode_id) + ",\"" + output_pin + "\"," + str(mode_id) + ",\"transmission\")\n" + lum_string = lum_string + "(" + str(len(freqs)) + ",3)\n" + freq_count = 0 + # for each frequency + for freq in freqs: + # write the frequency + lum_string = lum_string + '{:.12e}'.format(freq) + " " + mag = mags[freq_count, input_pin_count, output_pin_count] + angle = angles[freq_count, input_pin_count, output_pin_count] + # write the magnitude and phase of the s-parameter + lum_string = lum_string + '{:.12e}'.format(mag) + " " + lum_string = lum_string + '{:.12e}'.format(angle) + "\n" + freq_count += 1 + output_pin_count += 1 + input_pin_count += 1 + # write the string to the file + file_w = open(file_name, 'w') + file_w.write(lum_string) + file_w.close() + + class CircuitFormatter: """Base circuit formatter class that is extended to provide functionality for converting a circuit to a string and vice-versa.""" @@ -270,6 +384,15 @@ def parse(self, string: str) -> "Circuit": return components[0].circuit +class CircuitLumericalFormatter(CircuitFormatter): + + def format(self, circuit: "Circuit", freqs: np.array, orientations=None, file_name: str = None) -> None: + circuit_model = circuit.to_subcircuit(autoname=True) + model_formatter = ModelLumericalFormatter() + model_formatter.flatten_subcircuits = True + model_formatter.format(circuit_model, freqs, orientations, file_name) + + class CircuitSiEPICFormatter(CircuitFormatter): """This class saves/loads circuits in the SiEPIC SPICE format.""" diff --git a/simphony/layout.py b/simphony/layout.py index 8bd95fdb..c20462a9 100644 --- a/simphony/layout.py +++ b/simphony/layout.py @@ -183,12 +183,12 @@ def to_file( # restore the cwd os.chdir(cwd) - def to_subcircuit(self, name: str = "", **kwargs) -> "Subcircuit": + def to_subcircuit(self, name: str = "", autoname: bool = False, **kwargs) -> "Subcircuit": """Converts this circuit into a subcircuit component for easy re-use in another circuit.""" from simphony.models import Subcircuit - return Subcircuit(self, **kwargs, name=name) + return Subcircuit(self, **kwargs, name=name, autoname=autoname) @staticmethod def from_file( diff --git a/simphony/models.py b/simphony/models.py index 3dfce47e..94d1fdf3 100644 --- a/simphony/models.py +++ b/simphony/models.py @@ -463,6 +463,7 @@ def __init__( name: str = "", *, permanent: bool = True, + autoname: bool = False, **kwargs, ) -> None: """Initializes a subcircuit from the given circuit. @@ -492,7 +493,6 @@ def __init__( freq_range = [0, float("inf")] pins = [] pin_names = {} - for component in circuit: # calculate the frequency range for the subcircuit if component.freq_range[0] > freq_range[0]: @@ -504,6 +504,9 @@ def __init__( for pin in component.pins: # re-expose unconnected pins or pins connected to simulators if not pin._isconnected(include_simulators=False): + if autoname: + new_name = "port " + str(len(pins) + 1) + pin.name = new_name if permanent and pin.name in pin_names: raise ValueError( f"Multiple pins named '{pin.name}' cannot exist in a subcircuit." diff --git a/simphony/tools.py b/simphony/tools.py index 280796b6..2709884c 100644 --- a/simphony/tools.py +++ b/simphony/tools.py @@ -14,6 +14,7 @@ from scipy.constants import c as SPEED_OF_LIGHT from scipy.interpolate import interp1d +import numpy as np MATH_SUFFIXES = { "f": "e-15", @@ -120,7 +121,7 @@ def wl2freq(wl): return SPEED_OF_LIGHT / wl -def interpolate(resampled, sampled, s_parameters): +def interpolate(resampled, sampled, s_parameters, polar_interpolation=False): """Returns the result of a cubic interpolation for a given frequency range. Parameters @@ -131,6 +132,8 @@ def interpolate(resampled, sampled, s_parameters): A frequency array, indexed matching the given s_parameters. s_parameters : np.array S-parameters for each frequency given in input_freq. + polar_interpolation : bool + If True, the polar interpolation is used. Returns ------- @@ -138,5 +141,18 @@ def interpolate(resampled, sampled, s_parameters): The values of the interpolated function (fitted to the input s-parameters) evaluated at the ``output_freq`` frequencies. """ - func = interp1d(sampled, s_parameters, kind="cubic", axis=0) - return func(resampled) + + if polar_interpolation: + # convert to magnitude and phase + mag = np.abs(s_parameters) + angle = np.arctan2(s_parameters.imag, s_parameters.real) + angle = np.unwrap(angle, axis=0) + # interpolate + func_mag = interp1d(sampled, mag, kind='cubic', axis=0) + func_angle = interp1d(sampled, angle, kind='cubic', axis=0) + # convert back to complex and return + return func_mag(resampled) * np.cos(func_angle(resampled)) + \ + (func_mag(resampled) * np.sin(func_angle(resampled))) * 1j + else: + func = interp1d(sampled, s_parameters, kind="cubic", axis=0) + return func(resampled)