|
13 | 13 | from tidy3d.components.data.sim_data import SimulationData |
14 | 14 | from tidy3d.components.microwave.base import MicrowaveBaseModel |
15 | 15 | from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData |
| 16 | +from tidy3d.constants import C_0 |
| 17 | +from tidy3d.log import log |
16 | 18 | from tidy3d.plugins.smatrix.component_modelers.terminal import TerminalComponentModeler |
17 | 19 | from tidy3d.plugins.smatrix.data.base import AbstractComponentModelerData |
18 | | -from tidy3d.plugins.smatrix.data.data_array import PortDataArray, TerminalPortDataArray |
19 | | -from tidy3d.plugins.smatrix.ports.types import TerminalPortType |
| 20 | +from tidy3d.plugins.smatrix.data.data_array import ( |
| 21 | + PortDataArray, |
| 22 | + PortNameDataArray, |
| 23 | + TerminalPortDataArray, |
| 24 | +) |
| 25 | +from tidy3d.plugins.smatrix.ports.types import LumpedPortType, TerminalPortType |
20 | 26 | from tidy3d.plugins.smatrix.types import NetworkIndex, SParamDef |
21 | 27 | from tidy3d.plugins.smatrix.utils import ( |
22 | 28 | ab_to_s, |
@@ -117,6 +123,117 @@ def smatrix( |
117 | 123 | ) |
118 | 124 | return smatrix_data |
119 | 125 |
|
| 126 | + def change_port_reference_planes( |
| 127 | + self, smatrix: MicrowaveSMatrixData, port_shifts: PortNameDataArray = None |
| 128 | + ) -> MicrowaveSMatrixData: |
| 129 | + """ |
| 130 | + Performs S-parameter de-embedding by shifting reference planes ``port_shifts`` um. |
| 131 | +
|
| 132 | + Parameters |
| 133 | + ---------- |
| 134 | + smatrix : :class:`.MicrowaveSMatrixData` |
| 135 | + S-parameters before reference planes are shifted. |
| 136 | + port_shifts : :class:`.PortNameDataArray` |
| 137 | + Data array of shifts of wave ports' reference planes. |
| 138 | + The sign of a port shift reflects direction with respect to the axis normal to a ``WavePort`` plane: |
| 139 | + E.g.: ``PortNameDataArray(data=-a, coords={"port": "WP1"})`` defines a shift in the first ``WavePort`` by |
| 140 | + ``a`` um in the direction opposite to the positive axis direction (the axis normal to the port plane). |
| 141 | +
|
| 142 | + Returns |
| 143 | + ------- |
| 144 | + :class:`MicrowaveSMatrixData` |
| 145 | + De-embedded S-parameters with respect to updated reference frames. |
| 146 | + """ |
| 147 | + |
| 148 | + # get s-parameters with respect to current `WavePort` locations |
| 149 | + S_matrix = smatrix.data.values |
| 150 | + S_new = np.zeros_like(S_matrix, dtype=complex) |
| 151 | + N_freq, N_ports, _ = S_matrix.shape |
| 152 | + |
| 153 | + # pre-allocate memory for effective propagation constants |
| 154 | + kvecs = np.zeros((N_freq, N_ports), dtype=complex) |
| 155 | + shifts_vec = np.zeros(N_ports) |
| 156 | + directions_vec = np.ones(N_ports) |
| 157 | + |
| 158 | + port_idxs = [] |
| 159 | + n_complex_new = [] |
| 160 | + |
| 161 | + # extract raw data |
| 162 | + key = self.data.keys_tuple[0] |
| 163 | + data = self.data[key].data |
| 164 | + ports = self.modeler.ports |
| 165 | + |
| 166 | + # get port names and names of ports to be shifted |
| 167 | + port_names = [port.name for port in ports] |
| 168 | + shift_names = port_shifts.coords["port"].values |
| 169 | + |
| 170 | + # Build a mapping for quick lookup from monitor name to monitor data |
| 171 | + mode_map = {mode_data.monitor.name: mode_data for mode_data in data} |
| 172 | + |
| 173 | + # form a numpy vector of port shifts |
| 174 | + for shift_name in shift_names: |
| 175 | + # ensure that port shifts were defined for valid ports |
| 176 | + if shift_name not in port_names: |
| 177 | + raise ValueError( |
| 178 | + "The specified port could not be found in the simulation! " |
| 179 | + f"Please, make sure the port name is from the following list {port_names}" |
| 180 | + ) |
| 181 | + |
| 182 | + # get index of a shifted port in port_names list |
| 183 | + idx = port_names.index(shift_name) |
| 184 | + port = ports[idx] |
| 185 | + |
| 186 | + # if de-embedding is requested for lumped port |
| 187 | + if isinstance(port, LumpedPortType): |
| 188 | + raise ValueError( |
| 189 | + "De-embedding currently supports only 'WavePort' instances. " |
| 190 | + f"Received type: '{type(port).__name__}'." |
| 191 | + ) |
| 192 | + # alternatively we can send a warning and set `shifts_vector[index]` to 0. |
| 193 | + # shifts_vector[index] = 0.0 |
| 194 | + else: |
| 195 | + shifts_vec[idx] = port_shifts.sel(port=shift_name).values |
| 196 | + directions_vec[idx] = -1 if port.direction == "-" else 1 |
| 197 | + port_idxs.append(idx) |
| 198 | + |
| 199 | + # Collect corresponding mode_data |
| 200 | + mode_data = mode_map[port._mode_monitor_name] |
| 201 | + n_complex = mode_data.n_complex.sel(mode_index=port.mode_index) |
| 202 | + n_complex_new.append(np.squeeze(n_complex.data)) |
| 203 | + |
| 204 | + # flatten port shift vector |
| 205 | + shifts_vec = np.ravel(shifts_vec) |
| 206 | + directions_vec = np.ravel(directions_vec) |
| 207 | + |
| 208 | + # Convert to stacked arrays |
| 209 | + freqs = np.array(self.modeler.freqs) |
| 210 | + n_complex_new = np.array(n_complex_new).T |
| 211 | + |
| 212 | + # construct transformation matrix P_inv |
| 213 | + kvecs[:, port_idxs] = 2 * np.pi * freqs[:, np.newaxis] * n_complex_new / C_0 |
| 214 | + phase = -kvecs * shifts_vec * directions_vec |
| 215 | + P_inv = np.exp(1j * phase) |
| 216 | + |
| 217 | + # de-embed S-parameters: S_new = P_inv @ S_matrix @ P_inv |
| 218 | + S_new = S_matrix * P_inv[:, :, np.newaxis] * P_inv[:, np.newaxis, :] |
| 219 | + |
| 220 | + # create a new Port Data Array |
| 221 | + smat_data = TerminalPortDataArray(S_new, coords=smatrix.data.coords) |
| 222 | + |
| 223 | + return smatrix.updated_copy(data=smat_data) |
| 224 | + |
| 225 | + def smatrix_deembedded(self, port_shifts: np.ndarray = None) -> MicrowaveSMatrixData: |
| 226 | + """Interface function returns de-embedded S-parameter matrix.""" |
| 227 | + return self.change_port_reference_planes(self.smatrix(), port_shifts=port_shifts) |
| 228 | + |
| 229 | + @pd.root_validator(pre=False) |
| 230 | + def _warn_rf_license(cls, values): |
| 231 | + log.warning( |
| 232 | + "ℹ️ ⚠️ RF simulations are subject to new license requirements in the future. You have instantiated at least one RF-specific component.", |
| 233 | + log_once=True, |
| 234 | + ) |
| 235 | + return values |
| 236 | + |
120 | 237 | def _monitor_data_at_port_amplitude( |
121 | 238 | self, |
122 | 239 | port: TerminalPortType, |
|
0 commit comments