Source code for tidy3d.plugins.smatrix.component_modelers.terminal
"""Tool for generating an S matrix automatically from a Tidy3d simulation and lumped port definitions."""from__future__importannotationsfromtypingimportDict,Tuple,Unionimportnumpyasnpimportpydantic.v1aspdfrom....components.baseimportcached_propertyfrom....components.data.data_arrayimportDataArray,FreqDataArrayfrom....components.data.sim_dataimportSimulationDatafrom....components.geometry.utils_2dimportsnap_coordinate_to_gridfrom....components.simulationimportSimulationfrom....components.source.timeimportGaussianPulsefrom....components.typesimportAxfrom....components.vizimportadd_ax_if_none,equal_aspectfrom....constantsimportC_0,OHMfrom....exceptionsimportTidy3dError,ValidationErrorfrom....web.api.containerimportBatchDatafrom..ports.base_lumpedimportAbstractLumpedPortfrom..ports.base_terminalimportAbstractTerminalPort,TerminalPortDataArrayfrom..ports.coaxial_lumpedimportCoaxialLumpedPortfrom..ports.rectangular_lumpedimportLumpedPortfrom..ports.waveimportWavePortfrom.baseimportFWIDTH_FRAC,AbstractComponentModeler,TerminalPortTypeclassPortDataArray(DataArray):"""Array of values over dimensions of frequency and port name. Example ------- >>> f = [2e9, 3e9, 4e9] >>> ports = ["port1", "port2"] >>> coords = dict(f=f, port=ports) >>> fd = PortDataArray((1+1j) * np.random.random((3, 2)), coords=coords) """__slots__=()_dims=("f","port")
[docs]classTerminalComponentModeler(AbstractComponentModeler):"""Tool for modeling two-terminal multiport devices and computing port parameters with lumped and wave ports."""ports:Tuple[TerminalPortType,...]=pd.Field((),title="Terminal Ports",description="Collection of lumped and wave ports associated with the network. ""For each port, one simulation will be run with a source that is associated with the port.",)
[docs]@equal_aspect@add_ax_if_nonedefplot_sim(self,x:float=None,y:float=None,z:float=None,ax:Ax=None,**kwargs)->Ax:"""Plot a :class:`.Simulation` with all sources added for each port, for troubleshooting."""plot_sources=[]forport_sourceinself.ports:source_0=port_source.to_source(self._source_time)plot_sources.append(source_0)sim_plot=self.simulation.copy(update=dict(sources=plot_sources))returnsim_plot.plot(x=x,y=y,z=z,ax=ax,**kwargs)
[docs]@equal_aspect@add_ax_if_nonedefplot_sim_eps(self,x:float=None,y:float=None,z:float=None,ax:Ax=None,**kwargs)->Ax:"""Plot permittivity of the :class:`.Simulation` with all sources added for each port."""plot_sources=[]forport_sourceinself.ports:source_0=port_source.to_source(self._source_time)plot_sources.append(source_0)sim_plot=self.simulation.copy(update=dict(sources=plot_sources))returnsim_plot.plot_eps(x=x,y=y,z=z,ax=ax,**kwargs)
@cached_propertydefsim_dict(self)->Dict[str,Simulation]:"""Generate all the :class:`.Simulation` objects for the port parameter calculation."""sim_dict={}lumped_resistors=[port.to_load()forportinself._lumped_ports]# Create a mesh override for each port in case refinement is needed.# The port is a flat surface, but when computing the port current,# we'll eventually integrate the magnetic field just above and below# this surface, so the mesh override needs to ensure that the mesh# is fine enough not only in plane, but also in the normal direction.# So in the normal direction, we'll make sure there are at least# 2 cell layers above and below whose size is the same as the in-plane# cell size in the override region. Also, to ensure that the port itself# is aligned with a grid boundary in the normal direction, two separate# override regions are defined, one above and one below the analytical# port region.mesh_overrides=[]forport,lumped_resistorinzip(self._lumped_ports,lumped_resistors):ifport.num_grid_cells:mesh_overrides.extend(lumped_resistor.to_mesh_overrides())# also, use the highest frequency in the simulation to define the grid, rather than the# source's central frequency, to ensure an accurate solution over the entire rangegrid_spec=self.simulation.grid_spec.copy(update={"wavelength":C_0/np.max(self.freqs),"override_structures":list(self.simulation.grid_spec.override_structures)+mesh_overrides,})# Make an initial simulation with new grid_spec to determine where LumpedPorts are snappedsim_wo_source=self.simulation.updated_copy(grid_spec=grid_spec)snap_centers=dict()forportinself._lumped_ports:port_center_on_axis=port.center[port.injection_axis]new_port_center=snap_coordinate_to_grid(sim_wo_source.grid,port_center_on_axis,port.injection_axis)snap_centers[port.name]=new_port_center# Create monitors and snap to the center positionsfield_monitors=[monforportinself.portsformoninport.to_field_monitors(self.freqs,snap_center=snap_centers.get(port.name),grid=sim_wo_source.grid)]new_mnts=list(self.simulation.monitors)+field_monitorsnew_lumped_elements=list(self.simulation.lumped_elements)+[port.to_load(snap_center=snap_centers[port.name])forportinself._lumped_ports]update_dict=dict(monitors=new_mnts,lumped_elements=new_lumped_elements,)# This is the new default simulation will all shared components addedsim_wo_source=sim_wo_source.copy(update=update_dict)# Next, simulations are generated that include the source corresponding with the excitation portforportinself._lumped_ports:port_source=port.to_source(self._source_time,snap_center=snap_centers[port.name],grid=sim_wo_source.grid)task_name=self._task_name(port=port)sim_dict[task_name]=sim_wo_source.updated_copy(sources=[port_source])# Check final simulations for grid size at lumped portsfor_,siminsim_dict.items():TerminalComponentModeler._check_grid_size_at_ports(sim,self._lumped_ports)# Now, create simulations with wave port sources and mode solver monitors for computing port modesforwave_portinself._wave_ports:mode_monitor=wave_port.to_mode_solver_monitor(freqs=self.freqs)# Source is placed just before the field monitor of the portmode_src_pos=wave_port.center[wave_port.injection_axis]+self._shift_value_signed(wave_port)port_source=wave_port.to_source(self._source_time,snap_center=mode_src_pos)new_mnts_for_wave=new_mnts+[mode_monitor]update_dict=dict(monitors=new_mnts_for_wave,sources=[port_source])task_name=self._task_name(port=wave_port)sim_dict[task_name]=sim_wo_source.copy(update=update_dict)returnsim_dict@cached_propertydef_source_time(self):"""Helper to create a time domain pulse for the frequency range of interest."""freq0=np.mean(self.freqs)fdiff=max(self.freqs)-min(self.freqs)fwidth=max(fdiff,freq0*FWIDTH_FRAC)returnGaussianPulse(freq0=freq0,fwidth=fwidth,remove_dc_component=self.remove_dc_component)def_construct_smatrix(self)->TerminalPortDataArray:"""Post process :class:`.BatchData` to generate scattering matrix."""returnself._internal_construct_smatrix(batch_data=self.batch_data)def_internal_construct_smatrix(self,batch_data:BatchData)->TerminalPortDataArray:"""Post process :class:`.BatchData` to generate scattering matrix, for internal use only."""port_names=[port.nameforportinself.ports]values=np.zeros((len(self.freqs),len(port_names),len(port_names)),dtype=complex,)coords=dict(f=np.array(self.freqs),port_out=port_names,port_in=port_names,)a_matrix=TerminalPortDataArray(values,coords=coords)b_matrix=a_matrix.copy(deep=True)V_matrix=a_matrix.copy(deep=True)I_matrix=a_matrix.copy(deep=True)defport_VI(port_out:AbstractTerminalPort,sim_data:SimulationData):"""Helper to compute the port voltages and currents."""voltage=port_out.compute_voltage(sim_data)current=port_out.compute_current(sim_data)returnvoltage,current# Tabulate the reference impedances at each port and frequencyport_impedances=self.port_reference_impedances(batch_data=batch_data)# loop through source portsforport_ininself.ports:sim_data=batch_data[self._task_name(port=port_in)]forport_outinself.ports:V_out,I_out=port_VI(port_out,sim_data)V_matrix.loc[dict(port_in=port_in.name,port_out=port_out.name,)]=V_outI_matrix.loc[dict(port_in=port_in.name,port_out=port_out.name,)]=I_out# Reshape arrays so that broadcasting can be used to make F and Z act as diagonal matrices at each frequency# Ensure data arrays have the correct data layoutV_numpy=V_matrix.transpose(*TerminalPortDataArray._dims).valuesI_numpy=I_matrix.transpose(*TerminalPortDataArray._dims).valuesZ_numpy=port_impedances.transpose(*PortDataArray._dims).values.reshape((len(self.freqs),len(port_names),1))# Check to make sure sign is consistent for all impedance valuesself._check_port_impedance_sign(Z_numpy)# Check for negative real part of port impedance and flip the V and Z signs accordinglynegative_real_Z=np.real(Z_numpy)<0V_numpy=np.where(negative_real_Z,-V_numpy,V_numpy)Z_numpy=np.where(negative_real_Z,-Z_numpy,Z_numpy)F_numpy=TerminalComponentModeler._compute_F(Z_numpy)# Equation 4.67 - Pozar - Microwave Engineering 4eda_matrix.values=F_numpy*(V_numpy+Z_numpy*I_numpy)b_matrix.values=F_numpy*(V_numpy-np.conj(Z_numpy)*I_numpy)s_matrix=self.ab_to_s(a_matrix,b_matrix)returns_matrix@pd.validator("simulation")def_validate_3d_simulation(cls,val):"""Error if :class:`.Simulation` is not a 3D simulation"""ifval.size.count(0.0)>0:raiseValidationError(f"'{cls.__name__}' must be setup with a 3D simulation with all sizes greater than 0.")returnval@staticmethoddef_check_grid_size_at_ports(simulation:Simulation,ports:list[Union[AbstractLumpedPort]]):"""Raises :class:`.SetupError` if the grid is too coarse at port locations"""yee_grid=simulation.grid.yeeforportinports:port._check_grid_size(yee_grid)
[docs]@staticmethoddefcompute_power_wave_amplitudes(port:Union[LumpedPort,CoaxialLumpedPort],sim_data:SimulationData)->tuple[FreqDataArray,FreqDataArray]:"""Helper to compute the incident and reflected power wave amplitudes at a port for a given simulation result. The computed amplitudes have not been normalized. """voltage=port.compute_voltage(sim_data)current=port.compute_current(sim_data)# Amplitudes for the incident and reflected power wavesa=(voltage+port.impedance*current)/2/np.sqrt(np.real(port.impedance))b=(voltage-port.impedance*current)/2/np.sqrt(np.real(port.impedance))returna,b
[docs]@staticmethoddefcompute_power_delivered_by_port(port:Union[LumpedPort,CoaxialLumpedPort],sim_data:SimulationData)->FreqDataArray:"""Helper to compute the total power delivered to the network by a port for a given simulation result. Units of power are Watts. """a,b=TerminalComponentModeler.compute_power_wave_amplitudes(sim_data=sim_data,port=port)# Power delivered is the incident power minus the reflected powerreturn0.5*(np.abs(a)**2-np.abs(b)**2)
[docs]@staticmethoddefab_to_s(a_matrix:TerminalPortDataArray,b_matrix:TerminalPortDataArray)->TerminalPortDataArray:"""Get the scattering matrix given the power wave matrices."""# Ensure dimensions are ordered properlya_matrix=a_matrix.transpose(*TerminalPortDataArray._dims)b_matrix=b_matrix.transpose(*TerminalPortDataArray._dims)s_matrix=a_matrix.copy(deep=True)a_vals=s_matrix.copy(deep=True).valuesb_vals=b_matrix.copy(deep=True).valuess_vals=np.matmul(b_vals,AbstractComponentModeler.inv(a_vals))s_matrix.data=s_valsreturns_matrix
[docs]@staticmethoddefs_to_z(s_matrix:TerminalPortDataArray,reference:Union[complex,PortDataArray])->DataArray:"""Get the impedance matrix given the scattering matrix and a reference impedance."""# Ensure dimensions are ordered properlyz_matrix=s_matrix.transpose(*TerminalPortDataArray._dims).copy(deep=True)s_vals=z_matrix.valueseye=np.eye(len(s_matrix.port_out.values),len(s_matrix.port_in.values))ifisinstance(reference,PortDataArray):# From Equation 4.68 - Pozar - Microwave Engineering 4ed# Ensure that Zport, F, and Finv act as diagonal matrices when multiplying by left or rightshape_left=(len(s_matrix.f),len(s_matrix.port_out),1)shape_right=(len(s_matrix.f),1,len(s_matrix.port_in))Zport=reference.values.reshape(shape_right)F=TerminalComponentModeler._compute_F(Zport).reshape(shape_right)Finv=(1.0/F).reshape(shape_left)FinvSF=Finv*s_vals*FRHS=eye*np.conj(Zport)+FinvSF*ZportLHS=eye-FinvSFz_vals=np.matmul(AbstractComponentModeler.inv(LHS),RHS)else:# Simpler case when all port impedances are the samez_vals=(np.matmul(AbstractComponentModeler.inv(eye-s_vals),(eye+s_vals))*reference)z_matrix.data=z_valsreturnz_matrix
[docs]defport_reference_impedances(self,batch_data:BatchData)->PortDataArray:"""Tabulates the reference impedance of each port at each frequency."""port_names=[port.nameforportinself.ports]values=np.zeros((len(self.freqs),len(port_names)),dtype=complex,)coords=dict(f=np.array(self.freqs),port=port_names)port_impedances=PortDataArray(values,coords=coords)forportinself.ports:ifisinstance(port,WavePort):# Mode solver data for each wave port is stored in its associated SimulationDatasim_data_port=batch_data[self._task_name(port=port)]# WavePorts have a port impedance calculated from its associated modal field distribution# and is frequency dependent.impedances=port.compute_port_impedance(sim_data_port).valuesport_impedances.loc[dict(port=port.name)]=impedances.squeeze()else:# LumpedPorts have a constant reference impedanceport_impedances.loc[dict(port=port.name)]=np.full(len(self.freqs),port.impedance)port_impedances=TerminalComponentModeler._set_port_data_array_attributes(port_impedances)returnport_impedances
@staticmethoddef_compute_F(Z_numpy:np.array):"""Helper to convert port impedance matrix to F, which is used for computing generalized scattering parameters."""return1.0/(2.0*np.sqrt(np.real(Z_numpy)))@cached_propertydef_lumped_ports(self)->list[AbstractLumpedPort]:"""A list of all lumped ports in the ``TerminalComponentModeler``"""return[portforportinself.portsifisinstance(port,AbstractLumpedPort)]@cached_propertydef_wave_ports(self)->list[WavePort]:"""A list of all wave ports in the ``TerminalComponentModeler``"""return[portforportinself.portsifisinstance(port,WavePort)]@staticmethoddef_set_port_data_array_attributes(data_array:PortDataArray)->PortDataArray:"""Helper to set additional metadata for ``PortDataArray``."""data_array.name="Z0"returndata_array.assign_attrs(units=OHM,long_name="characteristic impedance")def_check_port_impedance_sign(self,Z_numpy:np.ndarray):"""Sanity check for consistent sign of real part of Z for each port across all frequencies."""forport_idxinrange(Z_numpy.shape[1]):port_Z=Z_numpy[:,port_idx,0]signs=np.sign(np.real(port_Z))ifnotnp.all(signs==signs[0]):raiseTidy3dError(f"Inconsistent sign of real part of Z detected for port {port_idx}. ""If you received this error, please create an issue in the Tidy3D ""github repository.")