|
| 1 | +"""Tool for generating an S matrix automatically from a Tidy3d simulation and lumped port definitions.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import numpy as np |
| 6 | + |
| 7 | +from tidy3d.components.data.sim_data import SimulationData |
| 8 | +from tidy3d.plugins.smatrix.data.terminal import PortDataArray, TerminalPortDataArray |
| 9 | +from tidy3d.plugins.smatrix.ports.wave import WavePort |
| 10 | +from tidy3d.plugins.smatrix.utils import _check_port_impedance_sign, _compute_F, compute_port_VI |
| 11 | + |
| 12 | + |
| 13 | +def _construct_smatrix(simulation) -> TerminalPortDataArray: |
| 14 | + """Post process :class:`.BatchData` to generate scattering matrix.""" |
| 15 | + return _internal_construct_smatrix(simulation=simulation) |
| 16 | + |
| 17 | + |
| 18 | +def _port_reference_impedances(simulation) -> PortDataArray: |
| 19 | + """Tabulates the reference impedance of each port at each frequency using the |
| 20 | + supplied :class:`.BatchData`. |
| 21 | + """ |
| 22 | + # TODO properly refactor, plugins data types should not have web methods. |
| 23 | + |
| 24 | + port_names = [port.name for port in simulation.ports] |
| 25 | + |
| 26 | + values = np.zeros( |
| 27 | + (len(simulation.freqs), len(port_names)), |
| 28 | + dtype=complex, |
| 29 | + ) |
| 30 | + coords = {"f": np.array(simulation.freqs), "port": port_names} |
| 31 | + port_impedances = PortDataArray(values, coords=coords) |
| 32 | + for port in simulation.ports: |
| 33 | + if isinstance(port, WavePort): |
| 34 | + # Mode solver data for each wave port is stored in its associated SimulationData |
| 35 | + sim_data_port = simulation.batch_data[simulation._task_name(port=port)] |
| 36 | + # WavePorts have a port impedance calculated from its associated modal field distribution |
| 37 | + # and is frequency dependent. |
| 38 | + impedances = port.compute_port_impedance(sim_data_port).values |
| 39 | + port_impedances.loc[{"port": port.name}] = impedances.squeeze() |
| 40 | + else: |
| 41 | + # LumpedPorts have a constant reference impedance |
| 42 | + port_impedances.loc[{"port": port.name}] = np.full( |
| 43 | + len(simulation.freqs), port.impedance |
| 44 | + ) |
| 45 | + |
| 46 | + port_impedances = simulation._set_port_data_array_attributes(port_impedances) |
| 47 | + return port_impedances |
| 48 | + |
| 49 | + |
| 50 | +def _internal_construct_smatrix(simulation) -> TerminalPortDataArray: |
| 51 | + """Post process :class:`.BatchData` to generate scattering matrix, for internal use only.""" |
| 52 | + from tidy3d.plugins.smatrix.utils import ab_to_s |
| 53 | + |
| 54 | + port_names = [port.name for port in simulation.ports] |
| 55 | + |
| 56 | + values = np.zeros( |
| 57 | + (len(simulation.freqs), len(port_names), len(port_names)), |
| 58 | + dtype=complex, |
| 59 | + ) |
| 60 | + coords = { |
| 61 | + "f": np.array(simulation.freqs), |
| 62 | + "port_out": port_names, |
| 63 | + "port_in": port_names, |
| 64 | + } |
| 65 | + a_matrix = TerminalPortDataArray(values, coords=coords) |
| 66 | + b_matrix = a_matrix.copy(deep=True) |
| 67 | + |
| 68 | + # Tabulate the reference impedances at each port and frequency |
| 69 | + port_impedances = _port_reference_impedances(simulation=simulation) |
| 70 | + |
| 71 | + # loop through source ports |
| 72 | + for port_in in simulation.ports: |
| 73 | + sim_data = simulation.batch_data[simulation._task_name(port=port_in)] |
| 74 | + a, b = simulation.compute_power_wave_amplitudes_at_each_port(port_impedances, sim_data) |
| 75 | + indexer = {"f": a.f, "port_in": port_in.name, "port_out": a.port} |
| 76 | + a_matrix.loc[indexer] = a |
| 77 | + b_matrix.loc[indexer] = b |
| 78 | + |
| 79 | + s_matrix = ab_to_s(a_matrix, b_matrix) |
| 80 | + return s_matrix |
| 81 | + |
| 82 | + |
| 83 | +def compute_power_wave_amplitudes_at_each_port( |
| 84 | + simulation, port_reference_impedances: PortDataArray, sim_data: SimulationData |
| 85 | +) -> tuple[PortDataArray, PortDataArray]: |
| 86 | + """Compute the incident and reflected power wave amplitudes at each port. |
| 87 | + The computed amplitudes have not been normalized. |
| 88 | +
|
| 89 | + Parameters |
| 90 | + ---------- |
| 91 | + port_reference_impedances : :class:`.PortDataArray` |
| 92 | + Reference impedance at each port. |
| 93 | + sim_data : :class:`.SimulationData` |
| 94 | + Results from the simulation. |
| 95 | +
|
| 96 | + Returns |
| 97 | + ------- |
| 98 | + tuple[:class:`.PortDataArray`, :class:`.PortDataArray`] |
| 99 | + Incident (a) and reflected (b) power wave amplitudes at each port. |
| 100 | + """ |
| 101 | + port_names = [port.name for port in simulation.ports] |
| 102 | + values = np.zeros( |
| 103 | + (len(simulation.freqs), len(port_names)), |
| 104 | + dtype=complex, |
| 105 | + ) |
| 106 | + coords = { |
| 107 | + "f": np.array(simulation.freqs), |
| 108 | + "port": port_names, |
| 109 | + } |
| 110 | + |
| 111 | + V_matrix = PortDataArray(values, coords=coords) |
| 112 | + I_matrix = V_matrix.copy(deep=True) |
| 113 | + a = V_matrix.copy(deep=True) |
| 114 | + b = V_matrix.copy(deep=True) |
| 115 | + |
| 116 | + for port_out in simulation.ports: |
| 117 | + V_out, I_out = compute_port_VI(port_out, sim_data) |
| 118 | + indexer = {"port": port_out.name} |
| 119 | + V_matrix.loc[indexer] = V_out |
| 120 | + I_matrix.loc[indexer] = I_out |
| 121 | + |
| 122 | + V_numpy = V_matrix.values |
| 123 | + I_numpy = I_matrix.values |
| 124 | + Z_numpy = port_reference_impedances.values |
| 125 | + |
| 126 | + # Check to make sure sign is consistent for all impedance values |
| 127 | + _check_port_impedance_sign(Z_numpy) |
| 128 | + |
| 129 | + # # Check for negative real part of port impedance and flip the V and Z signs accordingly |
| 130 | + negative_real_Z = np.real(Z_numpy) < 0 |
| 131 | + V_numpy = np.where(negative_real_Z, -V_numpy, V_numpy) |
| 132 | + Z_numpy = np.where(negative_real_Z, -Z_numpy, Z_numpy) |
| 133 | + |
| 134 | + F_numpy = _compute_F(Z_numpy) |
| 135 | + |
| 136 | + # Equation 4.67 - Pozar - Microwave Engineering 4ed |
| 137 | + a.values = F_numpy * (V_numpy + Z_numpy * I_numpy) |
| 138 | + b.values = F_numpy * (V_numpy - np.conj(Z_numpy) * I_numpy) |
| 139 | + |
| 140 | + return a, b |
0 commit comments