6
6
import numpy as np
7
7
from typing import Tuple
8
8
from ...components .data .dataset import FieldDataset , FieldTimeDataset , ModeSolverDataset
9
- from ...components .data .data_array import (
10
- ScalarFieldDataArray ,
11
- ScalarModeFieldDataArray ,
12
- ScalarFieldTimeDataArray ,
13
- )
9
+ from ...components .data .data_array import AbstractSpatialDataArray
14
10
from ...components .base import cached_property
15
11
from ...components .types import Axis , Direction
16
12
from ...components .geometry .base import Box
17
13
from ...components .validators import assert_line , assert_plane
18
14
from ...exceptions import DataError
19
15
20
16
21
- class AxisAlignedPathIntegral (Box ):
17
+ class AbstractAxesRH :
18
+ """Represents an axis-aligned right-handed coordinate system with one axis preferred."""
19
+
20
+ @cached_property
21
+ def main_axis (self ) -> Axis :
22
+ """Subclasses should implement this method."""
23
+ raise NotImplementedError ()
24
+
25
+ @cached_property
26
+ def remaining_axes (self ) -> Tuple [Axis , Axis ]:
27
+ """Axes in plane, ordered to maintain a right-handed coordinate system"""
28
+ axes = [0 , 1 , 2 ]
29
+ axes .pop (self .main_axis )
30
+ if self .main_axis == 1 :
31
+ return (axes [1 ], axes [0 ])
32
+ else :
33
+ return (axes [0 ], axes [1 ])
34
+
35
+
36
+ class AxisAlignedPathIntegral (AbstractAxesRH , Box ):
22
37
"""Class for defining the simplest type of path integral which is aligned with Cartesian axes."""
23
38
24
39
_line_validator = assert_line ()
25
40
26
41
extrapolate_to_endpoints : bool = pd .Field (
27
42
False ,
28
43
title = "Extrapolate to endpoints" ,
29
- description = "If the endpoints of the path integral terminate at or near an interface, the field is likely discontinuous. "
30
- "This option ignores fields outside the bounds of the integral." ,
44
+ description = "If the endpoints of the path integral terminate at or near a material interface, "
45
+ "the field is likely discontinuous. This option ignores fields outside and on the bounds "
46
+ "of the integral. Should be turned on when computing voltage between two conductors. " ,
31
47
)
32
48
33
49
snap_path_to_grid : bool = pd .Field (
@@ -37,15 +53,16 @@ class AxisAlignedPathIntegral(Box):
37
53
"a field. If enabled, the integration path will be snapped to the grid." ,
38
54
)
39
55
40
- def compute_integral (self , scalar_field ):
56
+ def compute_integral (self , scalar_field : AbstractSpatialDataArray ):
57
+ """Computes the defined integral given the input `scalar_field`."""
41
58
if not scalar_field .does_cover (self .bounds ):
42
59
raise DataError ("scalar field does not cover the integration domain" )
43
- coord = "xyz" [self .integration_axis ]
60
+ coord = "xyz" [self .main_axis ]
44
61
45
62
scalar_field = self ._get_field_along_path (scalar_field )
46
63
# Get the boundaries
47
- min_bound = self .bounds [0 ][self .integration_axis ]
48
- max_bound = self .bounds [1 ][self .integration_axis ]
64
+ min_bound = self .bounds [0 ][self .main_axis ]
65
+ max_bound = self .bounds [1 ][self .main_axis ]
49
66
50
67
if self .extrapolate_to_endpoints :
51
68
# Remove field outside the boundaries
@@ -62,16 +79,20 @@ def compute_integral(self, scalar_field):
62
79
coords_interp = np .concatenate ((coords_interp , coordinates ))
63
80
coords_interp = np .concatenate ((coords_interp , [max_bound ]))
64
81
coords_interp = {coord : coords_interp }
65
- # Use extrapolation for the 2 additional endpoints
82
+
83
+ # Use extrapolation for the 2 additional endpoints, unless there is only a single sample point
84
+ method = "linear"
85
+ if len (coordinates ) == 1 and self .extrapolate_to_endpoints :
86
+ method = "nearest"
66
87
scalar_field = scalar_field .interp (
67
- coords_interp , method = "linear" , kwargs = {"fill_value" : "extrapolate" }
88
+ coords_interp , method = method , kwargs = {"fill_value" : "extrapolate" }
68
89
)
69
90
return scalar_field .integrate (coord = coord )
70
91
71
92
def _get_field_along_path (
72
- self ,
73
- scalar_field : ScalarFieldDataArray | ScalarModeFieldDataArray | ScalarFieldTimeDataArray ,
74
- ) -> ScalarFieldDataArray | ScalarModeFieldDataArray | ScalarFieldTimeDataArray :
93
+ self , scalar_field : AbstractSpatialDataArray
94
+ ) -> AbstractSpatialDataArray :
95
+ """Returns a selection of the input `scalar_field` ready for integration."""
75
96
axis1 = self .remaining_axes [0 ]
76
97
axis2 = self .remaining_axes [1 ]
77
98
coord1 = "xyz" [axis1 ]
@@ -109,18 +130,11 @@ def _get_field_along_path(
109
130
return scalar_field
110
131
111
132
@cached_property
112
- def integration_axis (self ) -> Axis :
113
- """Axis for performing integration"""
133
+ def main_axis (self ) -> Axis :
134
+ """Axis for performing integration. """
114
135
val = next ((index for index , value in enumerate (self .size ) if value != 0 ), None )
115
136
return val
116
137
117
- @cached_property
118
- def remaining_axes (self ) -> Tuple [Axis , Axis ]:
119
- """Axes perpendicular to the voltage axis"""
120
- axes = [0 , 1 , 2 ]
121
- axes .pop (self .integration_axis )
122
- return (axes [0 ], axes [1 ])
123
-
124
138
125
139
class VoltageIntegralAA (AxisAlignedPathIntegral ):
126
140
"""Class for computing the voltage between two points defined by an axis-aligned line."""
@@ -133,7 +147,7 @@ class VoltageIntegralAA(AxisAlignedPathIntegral):
133
147
134
148
def compute_voltage (self , em_field : FieldDataset | ModeSolverDataset | FieldTimeDataset ):
135
149
"""Compute voltage along path defined by a line."""
136
- e_component = "xyz" [self .integration_axis ]
150
+ e_component = "xyz" [self .main_axis ]
137
151
field_name = f"E{ e_component } "
138
152
# Validate that the field is present
139
153
if field_name not in em_field :
@@ -148,22 +162,27 @@ def compute_voltage(self, em_field: FieldDataset | ModeSolverDataset | FieldTime
148
162
return voltage
149
163
150
164
151
- class CurrentIntegralAA (Box ):
165
+ class CurrentIntegralAA (AbstractAxesRH , Box ):
152
166
"""Class for computing conduction current via Ampere's Circuital Law on an axis-aligned loop."""
153
167
154
168
_plane_validator = assert_plane ()
155
169
156
170
sign : Direction = pd .Field (
157
171
"+" ,
158
- title = "Direction of contour integral." ,
159
- description = "Positive indicates V=Vb-Va where position b has a larger coordinate along the axis of integration." ,
172
+ title = "Direction of contour integral" ,
173
+ description = "Positive indicates current flowing in the positive normal axis direction." ,
174
+ )
175
+
176
+ extrapolate_to_endpoints : bool = pd .Field (
177
+ False ,
178
+ title = "Extrapolate to endpoints" ,
179
+ description = "This parameter is passed to `AxisAlignedPathIntegral` objects when computing the contour integral." ,
160
180
)
161
181
162
182
snap_contour_to_grid : bool = pd .Field (
163
183
False ,
164
- title = "" ,
165
- description = "It might be desireable to integrate exactly along the Yee grid associated with "
166
- "the fields. If enabled, the integration path will be snapped to the grid." ,
184
+ title = "Snap contour to grid" ,
185
+ description = "This parameter is passed to `AxisAlignedPathIntegral` objects when computing the contour integral." ,
167
186
)
168
187
169
188
def compute_current (self , em_field : FieldDataset | ModeSolverDataset | FieldTimeDataset ):
@@ -200,21 +219,11 @@ def compute_current(self, em_field: FieldDataset | ModeSolverDataset | FieldTime
200
219
return current
201
220
202
221
@cached_property
203
- def normal_axis (self ) -> Axis :
222
+ def main_axis (self ) -> Axis :
204
223
"""Axis normal to loop"""
205
224
val = next ((index for index , value in enumerate (self .size ) if value == 0 ), None )
206
225
return val
207
226
208
- @cached_property
209
- def remaining_axes (self ) -> Tuple [Axis , Axis ]:
210
- """Axes in integration plane, ordered to maintain a right-handed coordinate system"""
211
- axes = [0 , 1 , 2 ]
212
- axes .pop (self .normal_axis )
213
- if self .normal_axis == 1 :
214
- return (axes [1 ], axes [0 ])
215
- else :
216
- return (axes [0 ], axes [1 ])
217
-
218
227
def _to_path_integrals (self , h_horizontal , h_vertical ) -> Tuple [AxisAlignedPathIntegral , ...]:
219
228
ax1 = self .remaining_axes [0 ]
220
229
ax2 = self .remaining_axes [1 ]
@@ -255,14 +264,14 @@ def _to_path_integrals(self, h_horizontal, h_vertical) -> Tuple[AxisAlignedPathI
255
264
bottom = AxisAlignedPathIntegral (
256
265
center = path_center ,
257
266
size = path_size ,
258
- extrapolate_to_endpoints = False ,
267
+ extrapolate_to_endpoints = self . extrapolate_to_endpoints ,
259
268
snap_path_to_grid = self .snap_contour_to_grid ,
260
269
)
261
270
path_center [ax2 ] = top_bound
262
271
top = AxisAlignedPathIntegral (
263
272
center = path_center ,
264
273
size = path_size ,
265
- extrapolate_to_endpoints = False ,
274
+ extrapolate_to_endpoints = self . extrapolate_to_endpoints ,
266
275
snap_path_to_grid = self .snap_contour_to_grid ,
267
276
)
268
277
@@ -276,14 +285,14 @@ def _to_path_integrals(self, h_horizontal, h_vertical) -> Tuple[AxisAlignedPathI
276
285
left = AxisAlignedPathIntegral (
277
286
center = path_center ,
278
287
size = path_size ,
279
- extrapolate_to_endpoints = False ,
288
+ extrapolate_to_endpoints = self . extrapolate_to_endpoints ,
280
289
snap_path_to_grid = self .snap_contour_to_grid ,
281
290
)
282
291
path_center [ax1 ] = right_bound
283
292
right = AxisAlignedPathIntegral (
284
293
center = path_center ,
285
294
size = path_size ,
286
- extrapolate_to_endpoints = False ,
295
+ extrapolate_to_endpoints = self . extrapolate_to_endpoints ,
287
296
snap_path_to_grid = self .snap_contour_to_grid ,
288
297
)
289
298
0 commit comments