Photonic Design Examples

Here we show how COLNA can be used to model simple photonic circuits.

Note

Did you use COLNA to model a photonic circuit? Let us know and we will be very happy to add your example on this page.

The examples can be downloaded from COLNA’s github repository.

Mach-Zehnder Interferometer Weight

In this example we model a Mach-Zehnder interferometer (MZI) used as an optical transmission weight: By tuning the mode index in one of the MZI arms we set the transmission for a constant input signal. In this example a heater is used to tune the index through the thermo-optic effect. The figure below shows a sketch of the structure of our MZI interferometer.

_images/mzi_example.svg

We use a PhysicalNetwork to model our circuit. As we use our MZI as a constant transmission weight we are not interested in the time delay caused by the MZI, therefore all time delays are set to zero.

After setting the global parameters (wavelength, effective mode index in waveguide) we start by modeling the y-splitter. Due to fabrication imperfections the input power will not be split exactly equally between the two output ports of the splitter; the same is true for the phase relations. We create symbolic numbers for the splitting efficiency and phases and create the corresponding Device. The combiner is modeled in a similar way.

# Global defaults
wavelength = 1.55e-6
neff = 2.6

# Splitter configuration
splitter_length = 10e-6
splitter_phase_default = 2 * np.pi / wavelength * neff * splitter_length
splitter_efficiency00 = SymNum(name='split_{00}', default=1 / np.sqrt(2), product=True)
splitter_efficiency01 = SymNum(name='split_{01}', default=1 / np.sqrt(2), product=True)
splitter_phase00 = SymNum(name='split_{\phi00}', default=splitter_phase_default, product=False)
splitter_phase01 = SymNum(name='split_{\phi01}', default=splitter_phase_default, product=False)

splitter = Device('splitter')
splitter.init_from_phase_and_attenuation_matrix(np.array([[splitter_efficiency00, splitter_efficiency01]]),
                                                np.array([[splitter_phase00, splitter_phase01]]), delay=0)
splitter.add_input('i0')

The parameters of the two waveguide arms are defined:

# Waveguide Configuration
arm_length = 500e-6
default_phase = 2 * np.pi / wavelength * neff * arm_length
arm_0_amplitude = SymNum(name='arm0amp', default=1.0, product=True)
arm_0_phase = SymNum(name='arm0phase', default=default_phase, product=False)
arm_1_amplitude = SymNum(name='arm1amp', default=1.0, product=True)
arm_1_phase = SymNum(name='arm1phase', default=default_phase, product=False)

Next we are ready to assemble the network.

# Create Network
physnet = PhysicalNetwork()
physnet.add_device(splitter)
physnet.add_device(combiner)
physnet.add_devicelink(
    DeviceLink('splitter', 'combiner', 'o0', 'i0', phase=arm_0_phase, attenuation=arm_0_amplitude, delay=0))
physnet.add_devicelink(
    DeviceLink('splitter', 'combiner', 'o1', 'i1', phase=arm_1_phase, attenuation=arm_1_amplitude, delay=0))

# Visualize the network
physnet.visualize(path='./visualizations/mziweight', format='svg', full_graph=True)

You can visualize the network and as usual you can also extract the network equation:

_images/mziweight.svg

As we use a constant input wave we can use the Network.get_reduced_output() method to get the output.

# Evaluate and get HTML output
physnet.evaluate()
physnet.get_html_result('device:combiner:o0',path='./visualizations/equation.html')

# Get default output
output_name = physnet.outputs[0]
print(physnet.get_reduced_output(output_name))

The Network.get_reduced_output() takes a feed dictionary as an argument. With this we can test different MZI configurations and plot the corresponding data.


# Sweep Heater
dn_dT = 1e-4 # Thermooptic coefficient (index change per temperature change)
temperature_range = np.arange(0,20,0.01)

# Ideal splitter and combiner
amplitudes = np.zeros(shape=temperature_range.shape)
for i, temperature in enumerate(temperature_range):
    feed_dict = {'arm0phase': 2 * np.pi / wavelength * (neff+dn_dT*temperature) * arm_length}
    amp, phase, delay = physnet.get_reduced_output(output_name, feed_dict=feed_dict)
    amplitudes[i] = amp**2

plt.plot(temperature_range, amplitudes, label='Ideal Splitter and Combiner')

# Splitter non-ideal splitting ratio
amplitudes = np.zeros(shape=temperature_range.shape)
for i, temperature in enumerate(temperature_range):
    feed_dict = {'arm0phase': 2 * np.pi / wavelength * (neff+dn_dT*temperature) * arm_length, 'split_{01}':1/np.sqrt(2)*0.9, 'split_{00}':1/np.sqrt(2)}
    amp, phase, delay = physnet.get_reduced_output(output_name, feed_dict=feed_dict)
    amplitudes[i] = amp**2

plt.plot(temperature_range, amplitudes, label='Non-ideal splitter')

# Splitter and Combiner non-ideal
amplitudes = np.zeros(shape=temperature_range.shape)
for i, temperature in enumerate(temperature_range):
    feed_dict = {'arm0phase': 2 * np.pi / wavelength * (neff+dn_dT*temperature) * arm_length, 'split_{01}':1/np.sqrt(2)*0.9, 'split_{00}':1/np.sqrt(2),
                 'comb_{00}':1/np.sqrt(2), 'comb_{10}':1/np.sqrt(2)*0.8}
    amp, phase, delay = physnet.get_reduced_output(output_name, feed_dict=feed_dict)
    amplitudes[i] = amp**2

plt.plot(temperature_range, amplitudes, label='Non-ideal splitter and combiner')
plt.xlabel('Temperature')
plt.ylabel('|E|^2')
plt.legend(loc='best')
plt.grid()
plt.savefig('./visualizations/output_power.svg')

The results are shown below.

_images/output_power.svg

There are more parameters to optimize, here we just played around with a few of them. It would also be possible to perform a sensitivity analysis to estimate which parameters are most critical.