"""Defines the FDTD grid."""from__future__importannotationsfromtypingimportTuple,List,Unionimportnumpyasnpimportpydantic.v1aspdfrom..baseimportTidy3dBaseModelfrom..data.data_arrayimportDataArray,SpatialDataArray,ScalarFieldDataArrayfrom..data.datasetimportUnstructuredGridDatasetType,UnstructuredGridDatasetfrom..typesimportArrayFloat1D,Axis,InterpMethod,Literalfrom..geometry.baseimportBoxfrom...exceptionsimportSetupError# data type of one dimensional coordinate array.Coords1D=ArrayFloat1D
[docs]classCoords(Tidy3dBaseModel):"""Holds data about a set of x,y,z positions on a grid. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) """x:Coords1D=pd.Field(...,title="X Coordinates",description="1-dimensional array of x coordinates.")y:Coords1D=pd.Field(...,title="Y Coordinates",description="1-dimensional array of y coordinates.")z:Coords1D=pd.Field(...,title="Z Coordinates",description="1-dimensional array of z coordinates.")@propertydefto_dict(self):"""Return a dict of the three Coord1D objects as numpy arrays."""return{key:self.dict()[key]forkeyin"xyz"}@propertydefto_list(self):"""Return a list of the three Coord1D objects as numpy arrays."""returnlist(self.to_dict.values())def_interp_from_xarray(self,array:Union[SpatialDataArray,ScalarFieldDataArray],interp_method:InterpMethod,fill_value:Union[Literal["extrapolate"],float]="extrapolate",)->Union[SpatialDataArray,ScalarFieldDataArray]:""" Similar to ``xarrray.DataArray.interp`` with 2 enhancements: 1) Check if the coordinate of the supplied data are in monotonically increasing order. If they are, apply the faster ``assume_sorted=True``. 2) For axes of single entry, instead of error, apply ``isel()`` along the axis. Parameters ---------- array : Union[:class:`.SpatialDataArray`, :class:`.ScalarFieldDataArray`] Supplied scalar dataset interp_method : :class:`.InterpMethod` Interpolation method. fill_value : Union[Literal['extrapolate'], float] = "extrapolate" Value used to fill in for points outside the data range. If set to 'extrapolate', values will be extrapolated into those regions using the "nearest" method. Returns ------- Union[:class:`.SpatialDataArray`, :class:`.ScalarFieldDataArray`] The interpolated spatial dataset. Note ---- This method is called from a :class:`Coords` instance with the array to be interpolated as an argument, not the other way around. """# Check which axes need interpolation or selectioninterp_ax=[]isel_ax=[]foraxin"xyz":ifarray.sizes[ax]==1:isel_ax.append(ax)else:interp_ax.append(ax)# apply iselection for the axis containing single entryiflen(isel_ax)>0:array=array.isel({ax:[0]*len(self.to_dict[ax])foraxinisel_ax})array=array.assign_coords({ax:self.to_dict[ax]foraxinisel_ax})iflen(interp_ax)==0:returnarray# Apply interp for the restis_sorted=all(np.all(np.diff(array.coords[f])>0)forfininterp_ax)interp_param={"method":interp_method,"assume_sorted":is_sorted,"kwargs":{"bounds_error":False,"fill_value":fill_value,},}# Mark extrapolated points with nan's to fill in lateriffill_value=="extrapolate"andinterp_method!="nearest":interp_param["kwargs"]["fill_value"]=np.nan# interpolationinterp_array=array.interp({ax:self.to_dict[ax]foraxininterp_ax},**interp_param)# Fill in nan's with nearest valuesiffill_value=="extrapolate"andinterp_method!="nearest":interp_param["method"]="nearest"interp_param["kwargs"]["fill_value"]="extrapolate"nearest_array=array.interp({ax:self.to_dict[ax]foraxininterp_ax},**interp_param)interp_array.values[:]=np.where(np.isnan(interp_array.values),nearest_array.values,interp_array.values)returninterp_arraydef_interp_from_unstructured(self,array:UnstructuredGridDatasetType,interp_method:InterpMethod,fill_value:Union[Literal["extrapolate"],float]="extrapolate",)->SpatialDataArray:""" Interpolate from untructured grid onto a Cartesian one. Parameters ---------- array : Union[class:`.TriangularGridDataset`, class:`.TetrahedralGridDataset`] Supplied scalar dataset interp_method : :class:`.InterpMethod` Interpolation method. fill_value : Union[Literal['extrapolate'], float] = "extrapolate" Value used to fill in for points outside the data range. If set to 'extrapolate', values will be extrapolated into those regions using the "nearest" method. Returns ------- :class:`.SpatialDataArray` The interpolated spatial dataset. Note ---- This method is called from a :class:`Coords` instance with the array to be interpolated as an argument, not the other way around. """interp_array=array.interp(**{ax:self.to_dict[ax]foraxin"xyz"},method=interp_method,fill_value=fill_value)returninterp_array
[docs]defspatial_interp(self,array:Union[SpatialDataArray,ScalarFieldDataArray,UnstructuredGridDatasetType],interp_method:InterpMethod,fill_value:Union[Literal["extrapolate"],float]="extrapolate",)->Union[SpatialDataArray,ScalarFieldDataArray]:""" Similar to ``xarrray.DataArray.interp`` with 2 enhancements: 1) (if input data is an ``xarrray.DataArray``) Check if the coordinate of the supplied data are in monotonically increasing order. If they are, apply the faster ``assume_sorted=True``. 2) Data is assumed invariant along zero-size dimensions (if any). Parameters ---------- array : Union[ :class:`.SpatialDataArray`, :class:`.ScalarFieldDataArray`, :class:`.TriangularGridDataset`, :class:`.TetrahedralGridDataset`, ] Supplied scalar dataset interp_method : :class:`.InterpMethod` Interpolation method. fill_value : Union[Literal['extrapolate'], float] = "extrapolate" Value used to fill in for points outside the data range. If set to 'extrapolate', values will be extrapolated into those regions using the "nearest" method. Returns ------- Union[:class:`.SpatialDataArray`, :class:`.ScalarFieldDataArray`] The interpolated spatial dataset. Note ---- This method is called from a :class:`Coords` instance with the array to be interpolated as an argument, not the other way around. """# Check for empty dimensionsresult_coords=dict(self.to_dict)ifany(len(v)==0forvinresult_coords.values()):ifisinstance(array,(SpatialDataArray,ScalarFieldDataArray)):forcinarray.coords:ifcnotinresult_coords:result_coords[c]=array.coords[c].valuesresult_shape=tuple(len(v)forvinresult_coords.values())result=DataArray(np.empty(result_shape,dtype=array.dtype),coords=result_coords)returnresult# interpolationifisinstance(array,UnstructuredGridDataset):returnself._interp_from_unstructured(array=array,interp_method=interp_method,fill_value=fill_value)else:returnself._interp_from_xarray(array=array,interp_method=interp_method,fill_value=fill_value)
[docs]classFieldGrid(Tidy3dBaseModel):"""Holds the grid data for a single field. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> field_grid = FieldGrid(x=coords, y=coords, z=coords) """x:Coords=pd.Field(...,title="X Positions",description="x,y,z coordinates of the locations of the x-component of a vector field.",)y:Coords=pd.Field(...,title="Y Positions",description="x,y,z coordinates of the locations of the y-component of a vector field.",)z:Coords=pd.Field(...,title="Z Positions",description="x,y,z coordinates of the locations of the z-component of a vector field.",)
[docs]classYeeGrid(Tidy3dBaseModel):"""Holds the yee grid coordinates for each of the E and H positions. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> field_grid = FieldGrid(x=coords, y=coords, z=coords) >>> yee_grid = YeeGrid(E=field_grid, H=field_grid) >>> Ex_coords = yee_grid.E.x """E:FieldGrid=pd.Field(...,title="Electric Field Grid",description="Coordinates of the locations of all three components of the electric field.",)H:FieldGrid=pd.Field(...,title="Electric Field Grid",description="Coordinates of the locations of all three components of the magnetic field.",)@propertydefgrid_dict(self):"""The Yee grid coordinates associated to various field components as a dictionary."""return{"Ex":self.E.x,"Ey":self.E.y,"Ez":self.E.z,"Hx":self.H.x,"Hy":self.H.y,"Hz":self.H.z,}
[docs]classGrid(Tidy3dBaseModel):"""Contains all information about the spatial positions of the FDTD grid. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> grid = Grid(boundaries=coords) >>> centers = grid.centers >>> sizes = grid.sizes >>> yee_grid = grid.yee """boundaries:Coords=pd.Field(...,title="Boundary Coordinates",description="x,y,z coordinates of the boundaries between cells, defining the FDTD grid.",)@staticmethoddef_avg(coords1d:Coords1D):"""Return average positions of an array of 1D coordinates."""return(coords1d[1:]+coords1d[:-1])/2.0@staticmethoddef_min(coords1d:Coords1D):"""Return minus positions of 1D coordinates."""returncoords1d[:-1]@propertydefcenters(self)->Coords:"""Return centers of the cells in the :class:`Grid`. Returns ------- :class:`Coords` centers of the FDTD cells in x,y,z stored as :class:`Coords` object. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> grid = Grid(boundaries=coords) >>> centers = grid.centers """returnCoords(**{key:self._avg(val)forkey,valinself.boundaries.to_dict.items()})@propertydefsizes(self)->Coords:"""Return sizes of the cells in the :class:`Grid`. Returns ------- :class:`Coords` Sizes of the FDTD cells in x,y,z stored as :class:`Coords` object. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> grid = Grid(boundaries=coords) >>> sizes = grid.sizes """returnCoords(**{key:np.diff(val)forkey,valinself.boundaries.to_dict.items()})@propertydefnum_cells(self)->Tuple[int,int,int]:"""Return sizes of the cells in the :class:`Grid`. Returns ------- tuple[int, int, int] Number of cells in the grid in the x, y, z direction. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> grid = Grid(boundaries=coords) >>> Nx, Ny, Nz = grid.num_cells """return[len(self.boundaries.dict()[dim])-1fordimin"xyz"]@propertydef_primal_steps(self)->Coords:"""Return primal steps of the cells in the :class:`Grid`. Returns ------- :class:`Coords` Distances between each of the cell boundaries along each dimension. """returnself.sizes@propertydef_dual_steps(self)->Coords:"""Return dual steps of the cells in the :class:`Grid`. Returns ------- :class:`Coords` Distances between each of the cell centers along each dimension, with periodicity applied. """primal_steps={dim:self._primal_steps.dict()[dim]fordimin"xyz"}dsteps={key:(psteps+np.roll(psteps,1))/2for(key,psteps)inprimal_steps.items()}returnCoords(**dsteps)@propertydefyee(self)->YeeGrid:"""Return the :class:`YeeGrid` defining the yee cell locations for this :class:`Grid`. Returns ------- :class:`YeeGrid` Stores coordinates of all of the components on the yee lattice. Example ------- >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-1, 1, 11) >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> grid = Grid(boundaries=coords) >>> yee_cells = grid.yee >>> Ex_positions = yee_cells.E.x """yee_e_kwargs={key:self._yee_e(axis=axis)foraxis,keyinenumerate("xyz")}yee_h_kwargs={key:self._yee_h(axis=axis)foraxis,keyinenumerate("xyz")}yee_e=FieldGrid(**yee_e_kwargs)yee_h=FieldGrid(**yee_h_kwargs)returnYeeGrid(E=yee_e,H=yee_h)
[docs]def__getitem__(self,coord_key:str)->Coords:"""quickly get the grid element by grid[key]."""coord_dict={"centers":self.centers,"sizes":self.sizes,"boundaries":self.boundaries,"Ex":self.yee.E.x,"Ey":self.yee.E.y,"Ez":self.yee.E.z,"Hx":self.yee.H.x,"Hy":self.yee.H.y,"Hz":self.yee.H.z,}ifcoord_keynotincoord_dict:raiseSetupError(f"key {coord_key} not found in grid with {list(coord_dict.keys())} ")returncoord_dict.get(coord_key)
def_yee_e(self,axis:Axis):"""E field yee lattice sites for axis."""boundary_coords=self.boundaries.to_dict# initially set all to the minus boundsyee_coords={key:self._min(val)forkey,valinboundary_coords.items()}# average the axis index between the cell boundarieskey="xyz"[axis]yee_coords[key]=self._avg(boundary_coords[key])returnCoords(**yee_coords)def_yee_h(self,axis:Axis):"""H field yee lattice sites for axis."""boundary_coords=self.boundaries.to_dict# initially set all to centersyee_coords={key:self._avg(val)forkey,valinboundary_coords.items()}# set the axis index to the minus boundskey="xyz"[axis]yee_coords[key]=self._min(boundary_coords[key])returnCoords(**yee_coords)
[docs]defdiscretize_inds(self,box:Box,extend:bool=False)->List[Tuple[int,int]]:"""Start and stopping indexes for the cells that intersect with a :class:`Box`. Parameters ---------- box : :class:`Box` Rectangular geometry within simulation to discretize. extend : bool = False If ``True``, ensure that the returned indexes extend sufficiently in every direction to be able to interpolate any field component at any point within the ``box``, for field components sampled on the Yee grid. Returns ------- List[Tuple[int, int]] The (start, stop) indexes of the cells that intersect with ``box`` in each of the three dimensions. """pts_min,pts_max=box.boundsboundaries=self.boundariesinds_list=[]# for each dimensionforaxis,(pt_min,pt_max)inenumerate(zip(pts_min,pts_max)):bound_coords=np.array(boundaries.to_list[axis])assertpt_min<=pt_max,"min point was greater than max point"# index of smallest coord greater than pt_maxinds_gt_pt_max=np.where(bound_coords>pt_max)[0]ind_max=len(bound_coords)-1iflen(inds_gt_pt_max)==0elseinds_gt_pt_max[0]# index of largest coord less than or equal to pt_mininds_leq_pt_min=np.where(bound_coords<=pt_min)[0]ind_min=0iflen(inds_leq_pt_min)==0elseinds_leq_pt_min[-1]# handle extensionsifind_max>ind_minandextend:# Left sideifbox.bounds[0][axis]<self.centers.to_list[axis][ind_min]:# Box bounds on the left side are to the left of the closest grid centerind_min-=1# We always need an extra pixel on the right for the tangential componentsind_max+=1# store indexesinds_list.append((ind_min,ind_max))returninds_list
[docs]defextended_subspace(self,axis:Axis,ind_beg:int=0,ind_end:int=0,periodic:bool=True,)->Coords1D:"""Pick a subspace of 1D boundaries within ``range(ind_beg, ind_end)``. If any indexes lie outside of the grid boundaries array, padding is used based on the boundary conditions. For periodic BCs, the zeroth and last element of the grid boundaries are identified. For other BCs, the zeroth and last element of the boundaries are a reflection plane. Parameters ---------- axis : Axis Axis index along which to pick the subspace. ind_beg : int = 0 Starting index for the subspace. ind_end : int = 0 Ending index for the subspace. periodic : bool = True Whether to pad out of bounds indexes with a periodic or reflected pattern. Returns ------- Coords1D The subspace of the grid along ``axis``. """coords=self.boundaries.to_list[axis]padded_coords=coordsnum_cells=coords.size-1reverse=Truewhileind_beg<0:ifperiodicornotreverse:offset=padded_coords[0]-coords[-1]padded_coords=np.concatenate([coords[:-1]+offset,padded_coords])reverse=Trueelse:offset=padded_coords[0]+coords[0]padded_coords=np.concatenate([offset-coords[:0:-1],padded_coords])reverse=Falseind_beg+=num_cellsind_end+=num_cellsreverse=Truewhileind_end>=padded_coords.size:ifperiodicornotreverse:offset=padded_coords[-1]-coords[0]padded_coords=np.concatenate([padded_coords,coords[1:]+offset])reverse=Trueelse:offset=padded_coords[-1]+coords[-1]padded_coords=np.concatenate([padded_coords,offset-coords[-2::-1]])reverse=Falsereturnpadded_coords[ind_beg:ind_end]
[docs]defsnap_to_box_zero_dim(self,box:Box):"""Snap a grid to an exact box position for dimensions for which the box is size 0. If the box location is outside of the grid, an error is raised. Parameters ---------- box : :class:`Box` Box to use for the zero dim check. Returns ------- class:`Grid` Snapped copy of the grid. """boundary_dict=self.boundaries.to_dict.copy()fordim,center,sizeinzip("xyz",box.center,box.size):# Overwrite grid boundaries with box center if box is size 0 along dimensionifsize==0:ifboundary_dict[dim][0]>centerorboundary_dict[dim][-1]<center:raiseValueError("Cannot snap grid to box center outside of grid domain.")boundary_dict[dim]=np.array([center,center])returnself.updated_copy(boundaries=Coords(**boundary_dict))