Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lumerical formatter and new interpolation method #63

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 124 additions & 1 deletion simphony/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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()}."
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""

Expand Down
4 changes: 2 additions & 2 deletions simphony/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion simphony/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ def __init__(
name: str = "",
*,
permanent: bool = True,
autoname: bool = False,
**kwargs,
) -> None:
"""Initializes a subcircuit from the given circuit.
Expand Down Expand Up @@ -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]:
Expand All @@ -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."
Expand Down
22 changes: 19 additions & 3 deletions simphony/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -131,12 +132,27 @@ 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
-------
result : np.array
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)