Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Added S-parameter de-embedding to `TerminalComponentModelerData`, enabling recalculation with shifted reference planes.
- Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging.
- Added support for `symlog` and `log` scale plotting in `Scene.plot_eps()` and `Scene.plot_structures_property()` methods. The `symlog` scale provides linear behavior near zero and logarithmic behavior elsewhere, while 'log' is a base 10 logarithmic scale.
- Added `LowFrequencySmoothingSpec` and `ModelerLowFrequencySmoothingSpec` for automatic smoothing of mode monitor data at low frequencies where DFT sampling is insufficient.
Expand Down
64 changes: 64 additions & 0 deletions tests/test_plugins/smatrix/test_terminal_component_modeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
TerminalPortDataArray,
WavePort,
)
from tidy3d.plugins.smatrix.data.data_array import PortNameDataArray
from tidy3d.plugins.smatrix.ports.base_lumped import AbstractLumpedPort
from tidy3d.plugins.smatrix.utils import s_to_z, validate_square_matrix

Expand Down Expand Up @@ -1531,3 +1532,66 @@ def test_low_freq_smoothing_spec_sim_dict():
modeler = modeler.updated_copy(low_freq_smoothing=None)
for sim in modeler.sim_dict.values():
assert sim.low_freq_smoothing is None


def test_S_parameter_deembedding(monkeypatch, tmp_path):
"""Test S-parameter de-embedding."""

z_grid = td.UniformGrid(dl=1 * 1e3)
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
modeler = make_coaxial_component_modeler(port_types=(WavePort, WavePort), grid_spec=grid_spec)

# Make sure the smatrix and impedance calculations work for reduced simulations
modeler_data = run_component_modeler(monkeypatch, modeler)
s_matrix = modeler_data.smatrix()

# set up port shifts
port_names = [port.name for port in modeler.ports]
coords = {"port": port_names}
shift_vec = [0, 0]
port_shifts = PortNameDataArray(data=shift_vec, coords=coords)

# make sure that de-embedded S-matrices are identical to the original one if reference planes are not shifted
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
S_dmb_shortcut = modeler_data.smatrix_deembedded(port_shifts=port_shifts)
assert np.allclose(S_dmb.data.values, s_matrix.data.values)
assert np.allclose(S_dmb_shortcut.data.values, s_matrix.data.values)

# make sure S-parameters are different if reference planes are moved
port_shifts = PortNameDataArray(data=[-100, 200], coords=coords)
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
S_dmb_shortcut = modeler_data.smatrix_deembedded(port_shifts=port_shifts)
assert not np.allclose(S_dmb.data.values, s_matrix.data.values)
assert np.allclose(S_dmb.data.values, S_dmb_shortcut.data.values)

# test if `.smatrix_deembedded()` raises a `ValueError` when at least one port to be shifted is not defined in TCM
port_shifts_wrong = PortNameDataArray(data=[10, -10], coords={"port": ["wave_1", "LP_wave_2"]})

with pytest.raises(ValueError):
S_dmb = modeler_data.smatrix_deembedded(port_shifts=port_shifts_wrong)

# set up a new TCM with a mixture of `WavePort` and `CoaxialLumpedPort`
modeler_LP = make_coaxial_component_modeler(
port_types=(WavePort, CoaxialLumpedPort), grid_spec=grid_spec
)
modeler_data_LP = run_component_modeler(monkeypatch, modeler_LP)

# test if `.smatrix_deembedded()` raises a `ValueError` when one tries to de-embed a lumped port
port_shifts_LP = PortNameDataArray(data=[10, -10], coords={"port": ["wave_1", "coax_2"]})
with pytest.raises(ValueError):
S_dmb = modeler_data_LP.smatrix_deembedded(port_shifts=port_shifts_LP)

# update port shifts so that a reference plane is shifted only for `WavePort` port
port_shifts_LP = PortNameDataArray(data=[100], coords={"port": ["wave_1"]})

# get a new S-matrix
s_matrix_LP = modeler_data_LP.smatrix()

# de-embed S-matrix
S_dmb = modeler_data_LP.change_port_reference_planes(
smatrix=s_matrix_LP, port_shifts=port_shifts_LP
)
S_dmb_shortcut = modeler_data_LP.smatrix_deembedded(port_shifts=port_shifts_LP)
assert not np.allclose(S_dmb.data.values, s_matrix_LP.data.values)
assert np.allclose(S_dmb_shortcut.data.values, S_dmb.data.values)
16 changes: 16 additions & 0 deletions tidy3d/plugins/smatrix/data/data_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,19 @@ class TerminalPortDataArray(DataArray):
__slots__ = ()
_dims = ("f", "port_out", "port_in")
_data_attrs = {"long_name": "terminal-based port matrix element"}


class PortNameDataArray(DataArray):
"""Array of values indexed by port name.
Example
-------
>>> import numpy as np
>>> port_names = ["port1", "port2"]
>>> coords = dict(port_name=port_names)
>>> data = (1 + 1j) * np.random.random((2,))
>>> port_data = PortNameDataArray(data, coords=coords)
"""

__slots__ = ()
_dims = "port_name"
121 changes: 119 additions & 2 deletions tidy3d/plugins/smatrix/data/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@
from tidy3d.components.data.sim_data import SimulationData
from tidy3d.components.microwave.base import MicrowaveBaseModel
from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData
from tidy3d.constants import C_0
from tidy3d.log import log
from tidy3d.plugins.smatrix.component_modelers.terminal import TerminalComponentModeler
from tidy3d.plugins.smatrix.data.base import AbstractComponentModelerData
from tidy3d.plugins.smatrix.data.data_array import PortDataArray, TerminalPortDataArray
from tidy3d.plugins.smatrix.ports.types import TerminalPortType
from tidy3d.plugins.smatrix.data.data_array import (
PortDataArray,
PortNameDataArray,
TerminalPortDataArray,
)
from tidy3d.plugins.smatrix.ports.types import LumpedPortType, TerminalPortType
from tidy3d.plugins.smatrix.types import NetworkIndex, SParamDef
from tidy3d.plugins.smatrix.utils import (
ab_to_s,
Expand Down Expand Up @@ -117,6 +123,117 @@ def smatrix(
)
return smatrix_data

def change_port_reference_planes(
self, smatrix: MicrowaveSMatrixData, port_shifts: PortNameDataArray = None
) -> MicrowaveSMatrixData:
"""
Performs S-parameter de-embedding by shifting reference planes ``port_shifts`` um.

Parameters
----------
smatrix : :class:`.MicrowaveSMatrixData`
S-parameters before reference planes are shifted.
port_shifts : :class:`.PortNameDataArray`
Data array of shifts of wave ports' reference planes.
The sign of a port shift reflects direction with respect to the axis normal to a ``WavePort`` plane:
E.g.: ``PortNameDataArray(data=-a, coords={"port": "WP1"})`` defines a shift in the first ``WavePort`` by
``a`` um in the direction opposite to the positive axis direction (the axis normal to the port plane).

Returns
-------
:class:`MicrowaveSMatrixData`
De-embedded S-parameters with respect to updated reference frames.
"""

# get s-parameters with respect to current `WavePort` locations
S_matrix = smatrix.data.values
S_new = np.zeros_like(S_matrix, dtype=complex)
N_freq, N_ports, _ = S_matrix.shape

# pre-allocate memory for effective propagation constants
kvecs = np.zeros((N_freq, N_ports), dtype=complex)
shifts_vec = np.zeros(N_ports)
directions_vec = np.ones(N_ports)

port_idxs = []
n_complex_new = []

# extract raw data
key = self.data.keys_tuple[0]
data = self.data[key].data
ports = self.modeler.ports

# get port names and names of ports to be shifted
port_names = [port.name for port in ports]
shift_names = port_shifts.coords["port"].values

# Build a mapping for quick lookup from monitor name to monitor data
mode_map = {mode_data.monitor.name: mode_data for mode_data in data}

# form a numpy vector of port shifts
for shift_name in shift_names:
# ensure that port shifts were defined for valid ports
if shift_name not in port_names:
raise ValueError(
"The specified port could not be found in the simulation! "
f"Please, make sure the port name is from the following list {port_names}"
)

# get index of a shifted port in port_names list
idx = port_names.index(shift_name)
port = ports[idx]

# if de-embedding is requested for lumped port
if isinstance(port, LumpedPortType):
raise ValueError(
"De-embedding currently supports only 'WavePort' instances. "
f"Received type: '{type(port).__name__}'."
)
# alternatively we can send a warning and set `shifts_vector[index]` to 0.
# shifts_vector[index] = 0.0
else:
shifts_vec[idx] = port_shifts.sel(port=shift_name).values
directions_vec[idx] = -1 if port.direction == "-" else 1
port_idxs.append(idx)

# Collect corresponding mode_data
mode_data = mode_map[port._mode_monitor_name]
n_complex = mode_data.n_complex.sel(mode_index=port.mode_index)
n_complex_new.append(np.squeeze(n_complex.data))

# flatten port shift vector
shifts_vec = np.ravel(shifts_vec)
directions_vec = np.ravel(directions_vec)

# Convert to stacked arrays
freqs = np.array(self.modeler.freqs)
n_complex_new = np.array(n_complex_new).T

# construct transformation matrix P_inv
kvecs[:, port_idxs] = 2 * np.pi * freqs[:, np.newaxis] * n_complex_new / C_0
phase = -kvecs * shifts_vec * directions_vec
P_inv = np.exp(1j * phase)

# de-embed S-parameters: S_new = P_inv @ S_matrix @ P_inv
S_new = S_matrix * P_inv[:, :, np.newaxis] * P_inv[:, np.newaxis, :]

# create a new Port Data Array
smat_data = TerminalPortDataArray(S_new, coords=smatrix.data.coords)

return smatrix.updated_copy(data=smat_data)

def smatrix_deembedded(self, port_shifts: np.ndarray = None) -> MicrowaveSMatrixData:
"""Interface function returns de-embedded S-parameter matrix."""
return self.change_port_reference_planes(self.smatrix(), port_shifts=port_shifts)

@pd.root_validator(pre=False)
def _warn_rf_license(cls, values):
log.warning(
"ℹ️ ⚠️ RF simulations are subject to new license requirements in the future. You have instantiated at least one RF-specific component.",
log_once=True,
)
return values

def _monitor_data_at_port_amplitude(
self,
port: TerminalPortType,
Expand Down