SpectrumAnalyzer

class ipkiss3.all.SpectrumAnalyzer(smatrix, input_port_mode, output_port_modes, dB=True, peak_method='spline', peak_threshold=None, bandpass=None)

Tool to analyze the transmission spectra of an S-matrix.

Finds and stores passband (or stopband) information and provides measurement methods.

../../../_images/filter_measures.png
Parameters:

smatrix: SMatrix1DSweep

The smatrix sweep object

input_port_mode: str

The input port or port-mode. e.g. “in1”, “out1:0”

output_port_modes: List[str]

The output ports or port-modes e.g. [“out0”, “out1”, “out2”]

dB: bool

Whether to analyze the smatrix power in decibel (True) or linear scale (False)

peak_method: ‘spline’ or ‘cwt’

Peak detection method to be used. Please check i3.find_peaks for a detailed description on the available methods.

peak_threshold: float or None

Threshold for peak detection

bandpass: bool or None

Treat spectra as bandpass (True), bandstop (False) or automatic (None)

Examples

This example illustrates the difference between near_crosstalk and far_crosstalk:

import numpy as np
from matplotlib.offsetbox import AnchoredText
import matplotlib.pyplot as plt
from ipkiss3.simulation.circuit.results import SMatrix1DSweep
import ipkiss3.all as i3

# We first create some artificial 'signal' data
x = np.linspace(1, 100, 100)
peak = np.zeros((20,), dtype=np.complex128)

peak[:10] = np.linspace(0.5, 0.8, 10)
peak[10:20] = peak[9::-1]
yA = np.zeros((100,), dtype=np.complex128)
yA[5:25] = peak
yA[:6] = np.linspace(0.3, yA[5], 6)
yA[24:] = np.linspace(yA[24], 0.3, 76)

yB = np.zeros((100,), dtype=np.complex128)
yB[10:30] = peak
yB[:11] = np.linspace(0.3, yB[10], 11)
yB[29:] = np.linspace(yB[29], 0.3, 71)

yC = np.zeros((100,), dtype=np.complex128)
yC[15:35] = peak * 1.1
yC[:16] = np.linspace(0.3, yC[15], 16)
yC[34:] = np.linspace(yC[34], 0.3, 66)

yD = np.zeros((100,), dtype=np.complex128)
yD[50:60] = peak[:10]
yD[60:70] = 0.9
yD[70:80] = peak[10:]
yD[:51] = np.linspace(0.3, yD[50], 51)
yD[79:] = np.linspace(yD[79], 0.3, 21)

term_mode_map = {
    ("A", 0): 0,
    ("B", 0): 1,
    ("C", 0): 2,
    ("D", 0): 3,
}

smatrix = SMatrix1DSweep(
    n_ports=4,
    term_mode_map=term_mode_map,
    sweep_parameter_name="wavelength",
    sweep_parameter_values=x,
)

smatrix["A", "A", :] = yA
smatrix["A", "B", :] = yB
smatrix["B", "A", :] = yB

smatrix["A", "C", :] = yC
smatrix["C", "A", :] = yC

smatrix["A", "D", :] = yD
smatrix["D", "A", :] = yD

analyzer = i3.SpectrumAnalyzer(
    smatrix=smatrix,
    input_port_mode="A",
    output_port_modes=["A", "B", "C", "D"],
    dB=True,
)

# define the bands, you could use
# the cutoff_passband function to create these automatically
bands = {
    "A": [(10, 20)],
    "B": [(15, 25)],
    "C": [(20, 30)],
    "D": [(55, 75)],
}

# colors to use in our plots
colors = {
    "A": "blue",
    "B": "orange",
    "C": "limegreen",
    "D": "skyblue",
}

fig, (ax1, ax2) = plt.subplots(2, figsize=(15, 20))
# use the following if you prefer a column layout:
# fig, (ax1, ax2) = plt.subplots(1, 2)

# visualise the near_crosstalk of channel 'D'
ax1.add_artist(
    AnchoredText(
        "near_crosstalk of channel 'D'",
        frameon=True,
        loc="lower left",
    )
)

ax1.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "A"]),
    color=colors["A"],
    label="Channel A",
)

ax1.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "B"]),
    color=colors["B"],
    label="Channel B",
)

ax1.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "C"]),
    color=colors["C"],
    label="Channel C",
)

ax1.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "D"]),
    color=colors["D"],
    label="Channel D",
)

ax1.legend(loc='lower right')
peaks = analyzer.peaks()
peaks_x = peaks["D"]["wavelength"]
peak_powers = peaks["D"]["power"]
peak_power = peak_powers[0]

near_crosstalk = analyzer.near_crosstalk(bands)
ax1.axhline(y=peak_power + near_crosstalk["D"], color="darkgrey", linestyle=":")

for left, right in bands["D"]:
    ax1.axvline(x=left, color="red", linestyle="--")
    ax1.axvline(x=right, color="red", linestyle="--")

ax1.plot(
    bands["D"][0][0], peak_power + near_crosstalk["D"], 'ro',
)

ax1.annotate(
    "",
    xy=tuple((peaks_x[0], peak_power + near_crosstalk["D"])),
    xycoords="data",
    xytext=tuple((peaks_x[0], peak_power)),
    textcoords="data",
    arrowprops={"arrowstyle": "<->"},
    color="black",
)

ax1.annotate(
    "crosstalk={:.2f}dB".format(near_crosstalk["D"]),
    xy=(peaks_x[0] + 1, peak_power + near_crosstalk["D"] / 2.0),
    xycoords="data",
    textcoords="data",
)

# now visualise the far_crosstalk of channel 'D'
ax2.add_artist(
    AnchoredText(
        "far_crosstalk of channel 'D'",
        frameon=True,
        loc="lower left",
    )
)

ax2.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "A"]),
    color=colors["A"],
    label="Channel A",
)

ax2.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "B"]),
    color=colors["B"],
    label="Channel B",
)

ax2.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "C"]),
    color=colors["C"],
    label="Channel C",
)

ax2.plot(
    smatrix.sweep_parameter_values,
    i3.signal_power_dB(smatrix["A", "D"]),
    color=colors["D"],
    label="Channel D",
)

# far crosstalk calculates the max crosstalk with channels that
# that are not neighbouring channels.
# for channel 'D' this means the max crosstalk with channels 'A' and 'B',
# because channel 'C' is a neighbouring channel of 'D'.
crosstalk = analyzer.crosstalk_matrix(bands)
far_crosstalk = analyzer.far_crosstalk(bands)

# the far_crosstalk of channel 'D' equals the crosstalk in "D" due to "B"
# because it is bigger than the crosstalk in "D" due to "A"
assert far_crosstalk["D"] == crosstalk["D"]["B"]
assert crosstalk["D"]["B"] > crosstalk["D"]["A"]

ax2.axhline(y=peak_power + far_crosstalk["D"], color="darkgrey", linestyle=":")

for left, right in bands["D"]:
    ax2.axvline(x=left, color="red", linestyle="--")
    ax2.axvline(x=right, color="red", linestyle="--")

ax2.plot(
    bands["D"][0][0], peak_power + far_crosstalk["D"], 'ro',
)

ax2.annotate(
    "",
    xy=tuple((peaks_x[0], peak_power + far_crosstalk["D"])),
    xycoords="data",
    xytext=tuple((peaks_x[0], peak_power)),
    textcoords="data",
    arrowprops={"arrowstyle": "<->"},
    color="black",
)

ax2.annotate(
    "crosstalk={:.2f}dB".format(far_crosstalk["D"]),
    xy=(peaks_x[0] + 1, peak_power + far_crosstalk["D"] / 2.0),
    xycoords="data",
    textcoords="data",
)
plt.show()
../../../_images/ipkiss3-all-SpectrumAnalyzer-1.png
visualize(show=True, title=None)

Visualize the transmission spectra.

peaks()

Return the peaks per channel.

the peak data is stored as numpy record arrays. This allows you to use strings to access the values:

analyzer.peaks()['out1']['power'] 

Will give you only the power values of the peaks. Using ‘wavelength’ gives you the frequencies where the peaks are located.

analyzer.peaks()[‘out1’][‘wavelength’]
Returns:

A mapping from the output ports to their corresponding spectral peaks:

{'out1': [(peak1_x, peak1_power), (peak2_x, peak2_power)]}
find_peaks(method='spline', threshold=None, bandpass=None, store=True, **kwargs)

Uses i3.find_peaks to calculate a new list of peaks for every channel.

Parameters:

method: ‘spline’ or ‘cwt’

Peak finding method

threshold: float or None

Threshold power for peak finding. (autodetect if None)

bandpass: True or None

Interprete spectra as bandpass (True), bandstop (False) or autodetect (None)

store: bool

True: store the found peaks on the spectra. False: don’t store

Returns:

A mapping from the output ports to their corresponding spectral peaks:

{'out1': [(peak1_x, peak1_power), (peak2_x, peak2_power)]}
trim(band=None)

Trim the channels over a wavelength range of interest.

Parameters:

band

Lower and upper bound of x for the channels, i.e. (x_min, x_max). If no band is given, the whole spectrum is taken into account.

Returns:

New instance of SpectrumAnalyzer with trimmed channels

min_insertion_losses(bands=None)

Calculate the minimum insertion losses for every channel.

Parameters:

bands

Lower and upper bounds of x for the channels. If not specified, the whole spectrum will be used. Can be specified manually or calculated by means of cutoff_passbands or width_passbands.

Example where channel ‘out1’ has two passbands and ‘out2’ one passband over which we want to calculate the insertion loss:

{'out1': [(x_min1, x_max1), (x_min2, x_max2)], 'out2': [(x_min3, x_max3)]}
Returns:

A mapping from the output ports to their corresponding insertion losses (one per band):

{'out1': [-3.134, -3.132], 'out2': [-3.024]}
max_insertion_losses(bands=None)

Calculate the maximum insertion losses for every channel.

Parameters:

bands

Lower and upper bounds of x for the channels. If not specified, the whole spectrum will be used. Can be specified manually or calculated by means of cutoff_passbands or width_passbands.

Example where channel ‘out1’ has two passbands and ‘out2’ one passband over which we want to calculate the insertion loss:

{'out1': [(x_min1, x_max1), (x_min2, x_max2)], 'out2': [(x_min3, x_max3)]}
Returns:

A mapping from the output ports to their corresponding insertion losses (one per band):

{'out1': [-3.134, -3.132], 'out2': [-3.024]}
fsr()

Calculate the free spectral range for every channel.

Returns:

A mapping from the output ports to their corresponding FSR:

{'out1': 0.020, 'out2': 0.021}
width_passbands(width)

Calculate x-limited passbands centered around the peaks for every channel.

Parameters:

width: float

Width of one passband, expressed in the same units and scale as x.

Returns:

A mapping from the output ports to their corresponding passbands:

{'out1': [(x1, x2), (x3, x4)], 'out2': [(x5, x6)]}
cutoff_passbands(cutoff)

Calculate power-limited passbands centered around the peaks for every channel.

Parameters:

cutoff: float

Maximum acceptable power loss with respect to the peak power.

Returns:

A mapping from the output ports to their corresponding passbands:

{'out1': [(x1, x2), (x3, x4)], 'out2': [(x5, x6)]}
far_crosstalk(bands)

Calculate the maximum crosstalk for every channel, ignoring nearest neighbours.

Parameters:

bands

Lower and upper bounds of x for the channels. Can be specified manually or calculated by means of cutoff_passbands or width_passbands. Example where channel ‘out1’ has two passbands and ‘out2’ one passband over which we want to calculate the insertion loss:

{'out1': [(x_min1, x_max1), (x_min2, x_max2)], 'out2': [(x_min3, x_max3)]}
Returns:

A mapping from the output ports to their corresponding maximum crosstalks, ignoring nearest neighbours:

{'out1': -80.3, 'out2': -79.1}
near_crosstalk(bands)

Calculate the maximum crosstalk for every channel, taking only nearest neighbours into account.

Parameters:

bands

Lower and upper bounds of x for the channels. Can be specified manually or calculated by means of cutoff_passbands or width_passbands.

Example where channel ‘out1’ has two passbands and ‘out2’ one passband over which we want to calculate the insertion loss:

{'out1': [(x_min1, x_max1), (x_min2, x_max2)], 'out2': [(x_min3, x_max3)]}
Returns:

A mapping from the output ports to their corresponding maximum crosstalks,

taking only nearest neighbours into account:

{'out1': -80.3, 'out2': -79.1}
far_crosstalk_matrix(bands)

Calculate the crosstalk for every passband in every channel, ignoring nearest neighbours.

Parameters:

bands

Lower and upper bounds of x for the channels. Can be specified manually or calculated by means of cutoff_passbands or width_passbands.

Example where channel ‘out1’ has two passbands and ‘out2’ one passband over which we want to calculate the insertion loss:

{'out1': [(x_min1, x_max1), (x_min2, x_max2)], 'out2': [(x_min3, x_max3)]}
Returns:

A matrix in the form of a dictionary that maps two output ports to their corresponding crosstalk.

crosstalk_matrix[‘out1’][‘out3’] is the crosstalk in ‘out1’ caused by ‘out3’.

Because ‘out1’ has two passbands, the crosstalk also has two values. For example:

{'out1': {'out3': array([-83.03362741, -83.59168257]),
          'out4': array([-87.7717227 , -89.87154196]),
          'out5': array([-90.2991268 , -91.66997018]),
          'out6': array([-87.09402634, -87.20215863]),
          'out7': array([-83.25175028, -83.27741616])},
 ...}
near_crosstalk_matrix(bands)

Calculate the crosstalk for every passband in every channel, taking only nearest neighbours into account.

Parameters:

bands

Lower and upper bounds of x for the channels. Can be specified manually or calculated by means of SpectrumAnalyzer.cutoff_passbands() or SpectrumAnalyzer.width_passbands().

Example where channel ‘out1’ has two passbands and ‘out2’ one passband over which we want to calculate the insertion loss:

{'out1': [(x_min1, x_max1), (x_min2, x_max2)], 'out2': [(x_min3, x_max3)]}
Returns:

A matrix in the form of a dictionary that maps two output ports to their corresponding crosstalk.

crosstalk_matrix[‘out1’][‘out2’] is the crosstalk in ‘out1’ caused by ‘out2’.

Because ‘out1’ has two passbands, the crosstalk also has two values. For example:

{'out1': {'out2': array([-70.78072315, -74.29802755]),
          'out8': array([-72.40049707, -75.09575688])},
 ...}
crosstalk_matrix(bands)

Calculate the crosstalk for every passband in every channel.

Parameters:

bands

Lower and upper bounds of x for the channels. Can be specified manually or calculated by means of SpectrumAnalyzer.cutoff_passbands() or SpectrumAnalyzer.width_passbands().

Example where channel ‘out1’ has two passbands and ‘out2’ one passband over which we want to calculate the insertion loss:

{'out1': [(x_min1, x_max1), (x_min2, x_max2)], 'out2': [(x_min3, x_max3)]}
Returns:

A matrix in the form of a dictionary that maps two output ports to their corresponding crosstalk.

  • crosstalk_matrix[‘out1’][‘out3’] is the crosstalk in ‘out1’ caused by ‘out3’.
  • crosstalk_matrix[‘out1’][‘out1’] is the insertion loss variation (ripple) within the passbands of ‘out1’.

Because ‘out1’ has two passbands, the crosstalk also has two values (one for every passband). For example:

{'out1': {'out1': array([-1.7659547 , -2.14301945]),
          'out2': array([-70.78072315, -74.29802755]),
          'out3': array([-83.03362741, -83.59168257]),
          'out4': array([-87.7717227 , -89.87154196]),
          'out5': array([-90.2991268 , -91.66997018]),
          'out6': array([-87.09402634, -87.20215863]),
          'out7': array([-83.25175028, -83.27741616]),
          'out8': array([-72.40049707, -75.09575688])},
...}