Source code for tidy3d.plugins.smatrix.ports.coaxial_lumped
"""Lumped port specialization with an annular geometry for exciting coaxial ports."""importnumpyasnpimportpydantic.v1aspdfrom....components.baseimportcached_propertyfrom....components.data.data_arrayimportFreqDataArray,ScalarFieldDataArrayfrom....components.data.datasetimportFieldDatasetfrom....components.data.sim_dataimportSimulationDatafrom....components.geometry.baseimportBox,Geometryfrom....components.geometry.utils_2dimportincrement_floatfrom....components.grid.gridimportGrid,YeeGridfrom....components.lumped_elementimportCoaxialLumpedResistorfrom....components.monitorimportFieldMonitorfrom....components.source.currentimportCustomCurrentSourcefrom....components.source.timeimportGaussianPulsefrom....components.typesimportAxis,Coordinate,Direction,FreqArray,Sizefrom....components.validatorsimportskip_if_fields_missingfrom....constantsimportMICROMETERfrom....exceptionsimportSetupError,ValidationErrorfrom...microwaveimportCustomCurrentIntegral2D,VoltageIntegralAxisAlignedfrom...microwave.path_integralsimportAbstractAxesRHfrom.base_lumpedimportAbstractLumpedPortDEFAULT_COAX_SOURCE_NUM_POINTS=11
[docs]classCoaxialLumpedPort(AbstractLumpedPort,AbstractAxesRH):"""Class representing a single coaxial lumped port. Example ------- >>> port1 = CoaxialLumpedPort(center=(0, 0, 0), ... outer_diameter=4, ... inner_diameter=1, ... normal_axis=2, ... direction="+", ... name="coax_port_1", ... impedance=50 ... ) """center:Coordinate=pd.Field((0.0,0.0,0.0),title="Center",description="Center of object in x, y, and z.",units=MICROMETER,)outer_diameter:pd.PositiveFloat=pd.Field(...,title="Outer Diameter",description="Diameter of the outer coaxial circle.",units=MICROMETER,)inner_diameter:pd.PositiveFloat=pd.Field(...,title="Inner Diameter",description="Diameter of the inner coaxial circle.",units=MICROMETER,)normal_axis:Axis=pd.Field(...,title="Normal Axis",description="Specifies the axis which is normal to the concentric circles.",)direction:Direction=pd.Field(...,title="Direction",description="The direction of the signal travelling in the transmission line. ""This is needed in order to position the path integral, which is used for computing ""conduction current using Ampère's circuital law.",)@cached_propertydefmain_axis(self):"""Required for inheriting from AbstractAxesRH."""returnself.normal_axis@cached_propertydefinjection_axis(self):"""Required for inheriting from AbstractTerminalPort."""returnself.normal_axis@pd.validator("center",always=True)def_center_not_inf(cls,val):"""Make sure center is not infinity."""ifany(np.isinf(v)forvinval):raiseValidationError("'center' can not contain 'td.inf' terms.")returnval@pd.validator("inner_diameter",always=True)@skip_if_fields_missing(["outer_diameter"])def_ensure_inner_diameter_is_smaller(cls,val,values):"""Ensures that the inner diameter is smaller than the outer diameter, so that the final shape is an annulus."""outer_diameter=values.get("outer_diameter")ifval>=outer_diameter:raiseValidationError(f"The 'inner_diameter' {val} of a coaxial lumped element must be less than its "f"'outer_diameter' {outer_diameter}.")returnval
[docs]defto_source(self,source_time:GaussianPulse,snap_center:float=None,grid:Grid=None)->CustomCurrentSource:"""Create a current source from the lumped port."""# Discretized source amps are manually zeroed out later if they# fall on Yee grid locations outside the analytical source region.# Get transverse axestrans_axes=self.remaining_axes(coord1,coord2,coord3)=self.local_dimscenter=list(self.center)ifsnap_center:center[self.injection_axis]=snap_center# Figure out how many data points would be proper given the gridsize=[self.outer_diameter]*3size[self.injection_axis]=0bounding_box=Box(center=self.center,size=size)num1=DEFAULT_COAX_SOURCE_NUM_POINTSnum2=DEFAULT_COAX_SOURCE_NUM_POINTSifgrid:inds=grid.discretize_inds(box=bounding_box)num1=inds[trans_axes[0]][1]-inds[trans_axes[0]][0]num2=inds[trans_axes[1]][1]-inds[trans_axes[1]][0]# Get a normalized current density that is flowing radially from inner circle to outer circle# Total current is normalized to 1defcompute_coax_current(rin,rout,x,y):# Radial distancer=np.sqrt(x**2+y**2)# Remove division by 0r_valid=np.where(r==0.0,1,r)# Compute current density so that the total current# is 1 flowing through surfaces of constant r# Extra r is for changing to Cartesian coordinatesdenominator=2*np.pi*r_valid**2Jx=np.where(r<=rin,0,(x/denominator))Jx=np.where(r>=rout,0,Jx)Jy=np.where(r<=rin,0,(y/denominator))Jy=np.where(r>=rout,0,Jy)return(Jx,Jy)Router=self.outer_diameter/2Rinner=self.inner_diameter/2# Using a local reference frame of the port where the normal axis is along zxs,ys=np.linspace(-Router,Router,4*num1),np.linspace(-Router,Router,4*num2)x_grid,y_grid=np.meshgrid(xs,ys,indexing="ij")# Current density in a coaxial cable should flow radially with amplitude dropping with 1/rJx,Jy=compute_coax_current(Rinner,Router,x_grid,y_grid)# Package computed currents into datasetE1="E"+coord1E2="E"+coord2coord_vals={coord1:xs,coord2:ys,coord3:[center[self.injection_axis]],"f":[source_time.freq0],}kwargs={E1:ScalarFieldDataArray(Jx[...,None,None],coords=coord_vals,),E2:ScalarFieldDataArray(Jy[...,None,None],coords=coord_vals,),}dataset_E=FieldDataset(**kwargs)returnCustomCurrentSource(center=center,size=(self.outer_diameter,self.outer_diameter,0),source_time=source_time,name=self.name,interpolate=True,confine_to_bounds=True,current_dataset=dataset_E,)
[docs]defto_load(self,snap_center:float=None)->CoaxialLumpedResistor:"""Create a load resistor from the lumped port."""# 2D materials are currently snapped to the grid, so snapping here is not needed.# Snapping is done here so plots of the simulation will more accurately portray the setup.center=list(self.center)ifsnap_center:center[self.injection_axis]=snap_centerreturnCoaxialLumpedResistor(center=center,outer_diameter=self.outer_diameter,inner_diameter=self.inner_diameter,normal_axis=self.injection_axis,num_grid_cells=self.num_grid_cells,resistance=np.real(self.impedance),name=f"{self.name}_resistor",)
[docs]defto_voltage_monitor(self,freqs:FreqArray,snap_center:float=None,grid:Grid=None)->FieldMonitor:"""Field monitor to compute port voltage."""center=list(self.center)ifsnap_center:center[self.injection_axis]=snap_center# Get transverse dimensions(coord1,coord2)=self.remaining_dimsE1="E"+coord1E2="E"+coord2# Create a voltage monitorreturnFieldMonitor(center=self._voltage_path_center(center),size=self._voltage_path_size,freqs=freqs,fields=[E1,E2],name=self._voltage_monitor_name,colocate=False,)
[docs]defto_current_monitor(self,freqs:FreqArray,snap_center:float=None,grid:Grid=None)->FieldMonitor:"""Field monitor to compute port current."""center=list(self.center)ifsnap_center:center[self.injection_axis]=snap_center# Get transverse dimensions(coord1,coord2)=self.remaining_dimsH1="H"+coord1H2="H"+coord2# Size of current monitor needs to encompass the current carrying 2D sheet# Needs to have a nonzero thickness so a closed loop of gridpoints around the 2D sheet can be formeddl=2*(increment_float(center[self.injection_axis],1.0)-center[self.injection_axis])# Size of voltage monitor can essentially be 1D from ground to signal conductorcurrent_mon_size=[self.outer_diameter]*3current_mon_size[self.injection_axis]=dl# Create a current monitorreturnFieldMonitor(center=center,size=current_mon_size,freqs=freqs,fields=[H1,H2],name=self._current_monitor_name,colocate=False,)
[docs]defcompute_voltage(self,sim_data:SimulationData)->FreqDataArray:"""Helper to compute voltage across the port. We arbitrarily choose the positive first in-plane axis as the location for the path. Any of the four possible choices should give the same result. """exact_port_center=self.snapped_center(sim_data.simulation.grid)field_data=sim_data[self._voltage_monitor_name]voltage_integral=VoltageIntegralAxisAligned(center=self._voltage_path_center(exact_port_center),size=self._voltage_path_size,extrapolate_to_endpoints=True,snap_path_to_grid=True,sign="+",)voltage=voltage_integral.compute_voltage(field_data)# Return data array of voltage with coordinates of frequencyreturnvoltage
[docs]defcompute_current(self,sim_data:SimulationData)->FreqDataArray:"""Helper to compute current flowing through the port. The contour is a closed loop around the inner conductor. It is positioned at the midpoint between inner and outer radius of the annulus. """exact_port_center=self.snapped_center(sim_data.simulation.grid)# Loops around inner conductive circle conductorfield_data=sim_data[self._current_monitor_name]# Get transverse axes(coord1,coord2,coord3)=self.local_dims# Just need a rough estimate for the number of cellsfield_coords=field_data.field_components["H"+coord1].coords# Estimate number of points needed along path integralnum_coords_1=len(field_coords[coord1].values)num_coords_2=len(field_coords[coord2].values)num_coords=max(num_coords_1,num_coords_2)# Choose a number of points so that there are always points coincident# with the Cartesian axes (right, top, left, bottom).# So we round to the nearest multiple of 4.# One extra point is used to close the loop.num_path_coords=round(np.pi*num_coords/4)*4+1# Get the coordinates normal to port and select positions just on either side of the portnormal_coords=field_coords[coord3].valuesradius=(self.outer_diameter+self.inner_diameter)/4normal_port_position=exact_port_center[self.injection_axis]# The exact center position of the port should coincide with a Yee cell boundary, so we# want to select magnetic field positions a half-step on either side,# depending on the direction.path_pos=CoaxialLumpedPort._determine_current_integral_pos(normal_port_position,normal_coords,self.direction)# Setup the path integral and integrate the H fieldpath_center=list(exact_port_center)path_center[self.injection_axis]=path_pospath_integral=CustomCurrentIntegral2D.from_circular_path(path_center,radius,num_path_coords,self.injection_axis,False)current=path_integral.compute_current(field_data)# We need the current flowing transverse through the port, which is the opposite of# the current flowing in the core conductor in the positive direction.ifself.direction=="+":current*=-1.0returncurrent
@staticmethoddef_determine_current_integral_pos(snapped_center:float,normal_coords:np.array,direction:Direction)->float:"""Helper to locate where the current integral should be placed in relation to the normal axis of the port. """upper_bound=np.searchsorted(normal_coords,snapped_center)lower_bound=upper_bound-1# We need to choose which side of the port to place the path integral,ifdirection=="+":returnnormal_coords[upper_bound]else:returnnormal_coords[lower_bound]@cached_propertydef_voltage_axis(self)->Axis:"""Helper to return the chosen voltage axis. We arbitrarily choose the first in-plane axis."""returnself.remaining_axes[0]def_voltage_path_center(self,port_center:Coordinate)->Coordinate:"""We arbitrarily choose the positive first in-plane axis as the location for the path. Any of the four possible choices should give the same result. """center=list(port_center)center[self._voltage_axis]+=(self.outer_diameter+self.inner_diameter)/4returntuple(center)@cached_propertydef_voltage_path_size(self)->Size:"""We arbitrarily choose the positive first in-plane axis as the location for the path. Any of the four possible choices should give the same result. """axis_size=(self.outer_diameter-self.inner_diameter)/2size=Geometry.unpop_axis(axis_size,(0,0),self._voltage_axis)returnsizedef_check_grid_size(self,yee_grid:YeeGrid):"""Raises :class:``SetupError`` if the grid is too coarse at port locations"""trans_axes=self.remaining_axesforaxisintrans_axes:e_component="xyz"[trans_axes[0]]e_grid=yee_grid.grid_dict[f"E{e_component}"]coords=e_grid.to_dict[e_component]min_bound=self.center[axis]-self.outer_diameter/2max_bound=self.center[axis]-self.inner_diameter/2coords_within_port=np.any(np.logical_and(coords>min_bound,coords<max_bound))min_bound=self.center[axis]+self.inner_diameter/2max_bound=self.center[axis]+self.outer_diameter/2coords_within_port2=np.any(np.logical_and(coords>min_bound,coords<max_bound))ifnotcoords_within_portornotcoords_within_port2:raiseSetupError(f"Grid is too coarse along '{e_component}' direction for the lumped port "f"at location '{self.center}'. Either set the port's 'num_grid_cells' to "f"a nonzero integer or modify the 'GridSpec'. ")