1111)
1212from numpy .typing import NDArray
1313from omas import ODS
14+ from pytest import approx
1415from shapely import Point , Polygon
1516
1617from device_inductance import model_reduction
2223 _calc_plasma_coil_forces ,
2324 _calc_structure_coil_forces ,
2425)
26+ from device_inductance .grid import Extent , GridSpec , Resolution
2527from device_inductance .logging import log , logger_is_set_up , logger_setup_default
2628from device_inductance .mutuals import (
2729 _calc_circuit_mutual_inductances ,
@@ -79,13 +81,15 @@ class DeviceInductance:
7981 """OMAS data object in the format produced by device_description"""
8082 _max_nmodes : int = 40
8183 """Maximum number of structure modes to retain"""
82- _min_extent : tuple [ float , float , float , float ] = ( 0.0 , 0.0 , 0.0 , 0.0 )
84+ _min_extent : Extent | None = None
8385 """
8486 [m] rmin, rmax, zmin, zmax extent of computational domain.
8587 This will be updated during mesh initialization, during which it
8688 may be adjusted to satisfy the required spatial resolution.
8789 """
88- _dxgrid : tuple [float , float ] = (0.05 , 0.05 )
90+ _gridspec : GridSpec | None = None
91+ """Exact alternative to min_extent. Only one of min_extent or gridspec should be provided."""
92+ _dxgrid : Resolution = (0.05 , 0.05 )
8993 """[m] spatial resolution of computational grid"""
9094 _model_reduction_method : Literal ["eigenmode" , "stabilized eigenmode" ] = "eigenmode"
9195 """Choice of method for truncating passive structure system modes"""
@@ -106,7 +110,8 @@ def __init__(
106110 * ,
107111 show_prog : bool = False ,
108112 max_nmodes : int = 40 ,
109- min_extent : tuple [float , float , float , float ] = (0.0 , 0.0 , 0.0 , 0.0 ),
113+ min_extent : Extent | None = None ,
114+ gridspec : GridSpec | None = None ,
110115 dxgrid : tuple [float , float ] = (0.05 , 0.05 ),
111116 model_reduction_method : Literal ["eigenmode" , "stabilized eigenmode" ] = "eigenmode" ,
112117 plasma_coil_force_method : Literal ["tables" , "mask" ] = "mask" ,
@@ -123,6 +128,7 @@ def __init__(
123128 min_extent: [m] rmin, rmax, zmin, zmax extent of computational domain.
124129 This will be updated during mesh initialization, during which it
125130 may be adjusted to satisfy the required spatial resolution.
131+ gridspec: Exact alternative to min_extent. Only one of min_extent or gridspec should be provided.
126132 dxgrid: [m] spatial resolution of computational grid
127133 plasma_coil_force_method: Whether to interpolate B-field on the fully-realized mesh tables,
128134 or do direct filament calculations from points inside the limiter mask.
@@ -137,6 +143,7 @@ def __init__(
137143
138144 if "extent" in kwargs :
139145 # Backwards compatibility with `extent` kwarg name only
146+ log ().warning ("`extent` input is deprecated; use `min_extent` or `gridspec` instead" )
140147 min_extent = kwargs .pop ("extent" )
141148
142149 if len (kwargs ) != 0 :
@@ -145,6 +152,7 @@ def __init__(
145152 self ._ods = ods
146153 self ._max_nmodes = max_nmodes
147154 self ._min_extent = min_extent
155+ self ._gridspec = gridspec
148156 self ._dxgrid = dxgrid
149157 self ._model_reduction_method = model_reduction_method
150158 self ._show_prog = show_prog
@@ -158,8 +166,8 @@ def __hash__(self) -> int:
158166 return hash (id (self ))
159167
160168 def __post_init__ (self ):
161- # Set a sensible default that encompasses the coils with 0.1m pad if none was provided
162- if self ._min_extent == (0.0 , 0.0 , 0.0 , 0.0 ):
169+ # Set a sensible default grid that encompasses the coils with 0.1m pad if none was provided
170+ if ( self ._min_extent is None or self . _min_extent == (0.0 , 0.0 , 0.0 , 0.0 )) and self . _gridspec is None :
163171 inf = float ("inf" )
164172 self ._min_extent = (inf , - inf , inf , - inf )
165173 for c in self .coils :
@@ -172,6 +180,7 @@ def __post_init__(self):
172180
173181 # Set a sensible default grid resolution to avoid dividing by zero
174182 if self ._dxgrid == (0.0 , 0.0 ):
183+ log ().warning ("Replacing zero grid deltas with sensible default" )
175184 self ._dxgrid = (0.05 , 0.05 )
176185
177186 # Immutable after init, except for new cache entries
@@ -195,12 +204,25 @@ def max_nmodes(self) -> int:
195204 return self ._max_nmodes
196205
197206 @property
198- def min_extent (self ) -> tuple [ float , float , float , float ] :
207+ def min_extent (self ) -> Extent :
199208 """[m] (rmin, rmax, zmin, zmax) minimum extent requested at init.
200209 The actual extent of the mesh bounds this minimum, while respecting the
201- required grid resolution exactly.
210+ required grid resolution (and possibly gridspec) exactly.
202211 """
203- return self ._min_extent
212+ if self .gridspec is not None :
213+ rmin , nr , zmin , nz = self .gridspec
214+ dr , dz = self .dxgrid
215+ rmax = rmin + (nr - 1 ) * dr
216+ zmax = zmin + (nz - 1 ) * dz
217+ return (rmin , rmax , zmin , zmax )
218+ else :
219+ assert self ._min_extent is not None , "At least one of min_extent or gridspec must be provided"
220+ return self ._min_extent
221+
222+ @property
223+ def gridspec (self ) -> GridSpec | None :
224+ """Exact alternative to min_extent. Only one of min_extent or gridspec should be provided."""
225+ return self ._gridspec
204226
205227 @property
206228 def dxgrid (self ) -> tuple [float , float ]:
@@ -281,29 +303,44 @@ def limiter_mask(self) -> NDArray[F64]:
281303 @cached_property
282304 def _calc_meshes (
283305 self ,
284- ) -> tuple [tuple [NDArray [F64 ], NDArray [F64 ]], tuple [ float , float , float , float ] ]:
306+ ) -> tuple [tuple [NDArray [F64 ], NDArray [F64 ]], Extent ]:
285307 """Initialize both meshes and final extent after adjustment to achieve target resolution"""
286- # Actualize the grid/mesh and update extent
287- rmin , rmax , zmin , zmax = self .min_extent # [m]
288- dr , dz = self .dxgrid # [m]
289- if rmax - rmin > 0.0 and zmax - zmin > 0.0 :
290- rgrid = np .arange (rmin , rmax + dr , dr ) # [m] Grid that spans full extent
291- zgrid = np .arange (zmin , zmax + dz , dz ) # [m]
292- rmesh , zmesh = np .meshgrid (rgrid , zgrid , indexing = "ij" ) # [m]
308+ if self .gridspec is not None :
309+ # Make the grid
310+ rmin , nr , zmin , nz = self .gridspec
311+ extent = self .min_extent # [m] shrinkwraps gridspec
312+ rmin , rmax , zmin , zmax = extent # [m]
313+ rgrid = np .linspace (rmin , rmax , nr ) # [m]
314+ zgrid = np .linspace (zmin , zmax , nz ) # [m]
315+ # Check that the grid exactly matches the spec
316+ dr , dz = self .dxgrid
317+ assert rgrid [1 ] - rgrid [0 ] == approx (dr , rel = 1e-8 )
318+ assert zgrid [1 ] - zgrid [0 ] == approx (dz , rel = 1e-8 )
293319
294- # Update the extent, which may have been adjusted
295- extent = (
296- float (np .min (rgrid )),
297- float (np .max (rgrid )),
298- float (np .min (zgrid )),
299- float (np .max (zgrid )),
300- )
320+ rmesh , zmesh = np .meshgrid (rgrid , zgrid , indexing = "ij" ) # [m]
301321 else :
302- # If our grid spec is zero-size, make a unit mesh
303- # to allow the calcs to proceed, without providing real tables
304- rmesh = np .nan * np .zeros ((1 , 1 ))
305- zmesh = np .nan * np .zeros ((1 , 1 ))
306- extent = self .min_extent
322+ # Actualize the grid/mesh and update extent
323+ rmin , rmax , zmin , zmax = self .min_extent # [m]
324+ dr , dz = self .dxgrid # [m]
325+ if rmax - rmin > 0.0 and zmax - zmin > 0.0 :
326+ rgrid = np .arange (rmin , rmax + dr , dr ) # [m] Grid that spans full extent
327+ zgrid = np .arange (zmin , zmax + dz , dz ) # [m]
328+ rmesh , zmesh = np .meshgrid (rgrid , zgrid , indexing = "ij" ) # [m]
329+
330+ # Update the extent, which may have been adjusted
331+ extent = (
332+ float (np .min (rgrid )),
333+ float (np .max (rgrid )),
334+ float (np .min (zgrid )),
335+ float (np .max (zgrid )),
336+ )
337+ else :
338+ # If our grid spec is zero-size, make a unit mesh
339+ # to allow the calcs to proceed, without providing real tables
340+ rmesh = np .nan * np .zeros ((1 , 1 ))
341+ zmesh = np .nan * np .zeros ((1 , 1 ))
342+ extent = self .min_extent
343+
307344 return (rmesh , zmesh ), extent
308345
309346 @cached_property
@@ -321,13 +358,13 @@ def grids(self) -> tuple[NDArray[F64], NDArray[F64]]:
321358 return (rgrid , zgrid )
322359
323360 @cached_property
324- def extent (self ) -> tuple [ float , float , float , float ] :
361+ def extent (self ) -> Extent :
325362 """[m] The (rmin, rmax, zmin, zmax) extent of the grid cell centers"""
326363 _ , extent = self ._calc_meshes
327364 return extent
328365
329366 @cached_property
330- def extent_for_plotting (self ) -> tuple [ float , float , float , float ] :
367+ def extent_for_plotting (self ) -> Extent :
331368 """[m] The (rmin, rmax, zmin, zmax) extent of the grid cell edges"""
332369 rmin , rmax , zmin , zmax = self .extent
333370 dr , dz = self .dxgrid
@@ -829,13 +866,14 @@ class TypicalOutputs:
829866 on a device description ODS.
830867 """
831868
832- extent : tuple [ float , float , float , float ]
869+ extent : Extent
833870 """
834871 [m] Extent of the computational domain's cell centers.
835872 This will be updated during initialization, during which it
836873 may be adjusted to satisfy the required spatial resolution.
837874 """
838- extent_for_plotting : tuple [float , float , float , float ]
875+
876+ extent_for_plotting : Extent
839877 """
840878 [m] The full extent of the computational domain implied by
841879 the cell centers, expanded a half-cell to the boundary cells' edges
0 commit comments