emerge 0.6.7__py3-none-any.whl → 0.6.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of emerge might be problematic. Click here for more details.
- emerge/__init__.py +2 -2
- emerge/_emerge/_cache_check.py +1 -1
- emerge/_emerge/elements/femdata.py +3 -2
- emerge/_emerge/elements/index_interp.py +1 -2
- emerge/_emerge/elements/ned2_interp.py +16 -16
- emerge/_emerge/elements/nedelec2.py +17 -6
- emerge/_emerge/elements/nedleg2.py +21 -9
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/horn.py +0 -1
- emerge/_emerge/geo/modeler.py +1 -1
- emerge/_emerge/geo/operations.py +13 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +2 -3
- emerge/_emerge/geo/pmlbox.py +35 -11
- emerge/_emerge/geometry.py +4 -4
- emerge/_emerge/material.py +326 -81
- emerge/_emerge/mesh3d.py +14 -8
- emerge/_emerge/physics/microwave/assembly/assembler.py +43 -20
- emerge/_emerge/physics/microwave/microwave_3d.py +57 -44
- emerge/_emerge/physics/microwave/microwave_bc.py +10 -4
- emerge/_emerge/physics/microwave/microwave_data.py +61 -5
- emerge/_emerge/plot/pyvista/display.py +21 -11
- emerge/_emerge/plot/simple_plots.py +1 -1
- emerge/_emerge/simmodel.py +12 -5
- emerge/_emerge/solver.py +45 -18
- {emerge-0.6.7.dist-info → emerge-0.6.8.dist-info}/METADATA +1 -1
- {emerge-0.6.7.dist-info → emerge-0.6.8.dist-info}/RECORD +29 -29
- {emerge-0.6.7.dist-info → emerge-0.6.8.dist-info}/licenses/LICENSE +2 -2
- {emerge-0.6.7.dist-info → emerge-0.6.8.dist-info}/WHEEL +0 -0
- {emerge-0.6.7.dist-info → emerge-0.6.8.dist-info}/entry_points.txt +0 -0
emerge/_emerge/material.py
CHANGED
|
@@ -16,108 +16,353 @@
|
|
|
16
16
|
# <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import numpy as np
|
|
19
|
-
from dataclasses import dataclass
|
|
20
19
|
from typing import Callable
|
|
20
|
+
import inspect
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def num_args(func):
|
|
24
|
+
sig = inspect.signature(func)
|
|
25
|
+
return sum(
|
|
26
|
+
1
|
|
27
|
+
for p in sig.parameters.values()
|
|
28
|
+
if p.default is inspect._empty and p.kind in (
|
|
29
|
+
p.POSITIONAL_ONLY,
|
|
30
|
+
p.POSITIONAL_OR_KEYWORD,
|
|
31
|
+
p.KEYWORD_ONLY
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _to_mat(value: float | complex | int | np.ndarray) -> np.ndarray:
|
|
36
|
+
if np.isscalar(value):
|
|
37
|
+
return np.eye(3)*value
|
|
38
|
+
if value.shape in ((3,), (3,1), (1,3)):
|
|
39
|
+
return np.diag(np.ravel(value))
|
|
40
|
+
if value.shape == (3,3):
|
|
41
|
+
return value
|
|
42
|
+
else:
|
|
43
|
+
return ValueError(f'Trying to parse {value} as a material property tensor but it cant be identified as scalar, vector or matrix')
|
|
44
|
+
|
|
45
|
+
class MatProperty:
|
|
46
|
+
_freq_dependent: bool = False
|
|
47
|
+
_coord_dependent: bool = False
|
|
48
|
+
"""The MatProperty class is an interface for EMerge to deal with frequency and coordinate dependent material properties
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, value: float | complex | int | np.ndarray):
|
|
52
|
+
self.value: np.ndarray = _to_mat(value)
|
|
53
|
+
|
|
54
|
+
self._apply_to: np.ndarray = np.array([], dtype=np.int64)
|
|
55
|
+
self._x: np.ndarray = np.array([], dtype=np.float64)
|
|
56
|
+
self._y: np.ndarray = np.array([], dtype=np.float64)
|
|
57
|
+
self._z: np.ndarray = np.array([], dtype=np.float64)
|
|
58
|
+
|
|
59
|
+
self._fmax = lambda f: value
|
|
60
|
+
|
|
61
|
+
def initialize(self, x: np.ndarray, y: np.ndarray, z: np.ndarray, ids: np.ndarray) -> None:
|
|
62
|
+
self._apply_to = np.concatenate([self._apply_to, ids])
|
|
63
|
+
self._x = np.concatenate([self._x, x])
|
|
64
|
+
self._y = np.concatenate([self._y, y])
|
|
65
|
+
self._z = np.concatenate([self._z, z])
|
|
66
|
+
|
|
67
|
+
def __call__(self, f: float, data: np.ndarray) -> np.ndarray:
|
|
68
|
+
data[:,:,self._apply_to] = np.repeat(self.value[:,:,np.newaxis], self._apply_to.shape[0], axis=2)
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
def scalar(self, f: float):
|
|
72
|
+
return self.value[0,0]
|
|
73
|
+
|
|
74
|
+
def reset(self) -> None:
|
|
75
|
+
self._apply_to: np.ndarray = np.array([], dtype=np.int64)
|
|
76
|
+
self._x: np.ndarray = np.array([], dtype=np.float64)
|
|
77
|
+
self._y: np.ndarray = np.array([], dtype=np.float64)
|
|
78
|
+
self._z: np.ndarray = np.array([], dtype=np.float64)
|
|
79
|
+
|
|
80
|
+
class FreqDependent(MatProperty):
|
|
81
|
+
_freq_dependent: bool = True
|
|
82
|
+
_coord_dependent: bool = False
|
|
83
|
+
|
|
84
|
+
def __init__(self,
|
|
85
|
+
scalar: Callable | None = None,
|
|
86
|
+
vector: Callable | None = None,
|
|
87
|
+
matrix: Callable | None = None):
|
|
88
|
+
"""Creates a frequency dependent property object.
|
|
89
|
+
|
|
90
|
+
If the property is defined as a scalar value, use the "scalar" argument
|
|
91
|
+
If the property is a diagonal rank-2 tensor, use the "vector" argument
|
|
92
|
+
If the property is a full rank-2 tensor, use the "matrix" argument
|
|
93
|
+
|
|
94
|
+
The max_value property must be set to tell EMerge how height this value can get
|
|
95
|
+
as it will be used to define the discretization of the mesh.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
scalar (Callable | None, optional): The scalar value function returning a float/complex. Defaults to None.
|
|
99
|
+
vector (Callable | None, optional): The diagonal rank-2 tensor function returning a (3,) array. Defaults to None.
|
|
100
|
+
matrix (Callable | None, optional): The rank-2 tensor function returning a (3,3) array. Defaults to None.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
_type_: _description_
|
|
104
|
+
"""
|
|
105
|
+
if scalar is not None:
|
|
106
|
+
def _func(f: float) -> np.ndarray:
|
|
107
|
+
return np.eye(3)*scalar(f)
|
|
108
|
+
if vector is not None:
|
|
109
|
+
def _func(f: float) -> np.ndarray:
|
|
110
|
+
return np.diag(np.ravel(vector(f)))
|
|
111
|
+
|
|
112
|
+
if matrix is not None:
|
|
113
|
+
_func = matrix
|
|
114
|
+
|
|
115
|
+
self._func: Callable = _func
|
|
116
|
+
|
|
117
|
+
self._apply_to: np.ndarray = np.array([], dtype=np.int64)
|
|
118
|
+
self._x: np.ndarray = np.array([], dtype=np.float64)
|
|
119
|
+
self._y: np.ndarray = np.array([], dtype=np.float64)
|
|
120
|
+
self._z: np.ndarray = np.array([], dtype=np.float64)
|
|
121
|
+
|
|
122
|
+
self._fmax: Callable = lambda f: np.max(np.ravel(self._func(f)))
|
|
123
|
+
|
|
124
|
+
def initialize(self, x: np.ndarray, y: np.ndarray, z: np.ndarray, ids: np.ndarray) -> None:
|
|
125
|
+
self._apply_to = np.concatenate([self._apply_to, ids])
|
|
126
|
+
|
|
127
|
+
def __call__(self, f: float, data: np.ndarray) -> np.ndarray:
|
|
128
|
+
data[:,:,self._apply_to] = np.repeat(self._func(f)[:,:,np.newaxis], self._apply_to.shape[0], axis=2)
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
def scalar(self, f: float):
|
|
132
|
+
return self._func(f)[0,0]
|
|
133
|
+
|
|
134
|
+
class CoordDependent(MatProperty):
|
|
135
|
+
_freq_dependent: bool = False
|
|
136
|
+
_coord_dependent: bool = True
|
|
137
|
+
def __init__(self,
|
|
138
|
+
max_value: float,
|
|
139
|
+
scalar: Callable | None = None,
|
|
140
|
+
vector: Callable | None = None,
|
|
141
|
+
matrix: Callable | None = None,
|
|
142
|
+
):
|
|
143
|
+
"""Creates a coordinate dependent property object.
|
|
144
|
+
|
|
145
|
+
If the property is defined as a scalar value, use the "scalar" argument.
|
|
146
|
+
|
|
147
|
+
If the property is a diagonal rank-2 tensor, use the "vector" argument.
|
|
148
|
+
|
|
149
|
+
If the property is a full rank-2 tensor, use the "matrix" argument.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
The max_value property must be set to tell EMerge how height this value can get
|
|
153
|
+
as it will be used to define the discretization of the mesh.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
max_value (float): The heighest value of the material property
|
|
157
|
+
scalar (Callable | None, optional): The scalar value function returning a float/complex. Defaults to None.
|
|
158
|
+
vector (Callable | None, optional): The diagonal rank-2 tensor function returning a (3,) array. Defaults to None.
|
|
159
|
+
matrix (Callable | None, optional): The rank-2 tensor function returning a (3,3) array. Defaults to None.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
_type_: _description_
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
if scalar is not None:
|
|
166
|
+
def _func(x, y, z) -> np.ndarray:
|
|
167
|
+
return np.eye(3)[:, :, None] * scalar(x,y,z)[None, None, :]
|
|
168
|
+
if vector is not None:
|
|
169
|
+
def _func(x, y, z) -> np.ndarray:
|
|
170
|
+
N = x.shape[0]
|
|
171
|
+
out = np.zeros((3, 3, N), dtype=vector(0,0,0).dtype)
|
|
172
|
+
idx = np.arange(3)
|
|
173
|
+
out[idx, idx, :] = vector(x,y,z)
|
|
174
|
+
return out
|
|
175
|
+
if matrix is not None:
|
|
176
|
+
_func = matrix
|
|
177
|
+
|
|
178
|
+
self._func: Callable = _func
|
|
179
|
+
self._apply_to: np.ndarray = np.array([], dtype=np.int64)
|
|
180
|
+
self._x: np.ndarray = np.array([], dtype=np.float64)
|
|
181
|
+
self._y: np.ndarray = np.array([], dtype=np.float64)
|
|
182
|
+
self._z: np.ndarray = np.array([], dtype=np.float64)
|
|
183
|
+
|
|
184
|
+
self._values: np.ndarray = None
|
|
185
|
+
self._fmax: Callable = lambda f: max_value
|
|
186
|
+
|
|
187
|
+
def initialize(self, x: np.ndarray, y: np.ndarray, z: np.ndarray, ids: np.ndarray) -> None:
|
|
188
|
+
self._apply_to = np.concatenate([self._apply_to, ids])
|
|
189
|
+
self._x = np.concatenate([self._x, x])
|
|
190
|
+
self._y = np.concatenate([self._y, y])
|
|
191
|
+
self._z = np.concatenate([self._z, z])
|
|
192
|
+
|
|
193
|
+
def __call__(self, f: float, data: np.ndarray) -> np.ndarray:
|
|
194
|
+
data[:,:,self._apply_to] = self._func(self._x, self._y, self._z)
|
|
195
|
+
return data
|
|
196
|
+
|
|
197
|
+
def scalar(self, f: float):
|
|
198
|
+
return self._func(0,0,0)[0,0]
|
|
199
|
+
|
|
200
|
+
class FreqCoordDependent(MatProperty):
|
|
201
|
+
_freq_dependent: bool = True
|
|
202
|
+
_coord_dependent: bool = True
|
|
203
|
+
|
|
204
|
+
def __init__(self,
|
|
205
|
+
max_value: float,
|
|
206
|
+
scalar: Callable | None = None,
|
|
207
|
+
vector: Callable | None = None,
|
|
208
|
+
matrix: Callable | None = None):
|
|
209
|
+
"""Creates a frequency and coordinate dependent property object.
|
|
210
|
+
|
|
211
|
+
If the property is defined as a scalar value, use the "scalar" argument.
|
|
212
|
+
|
|
213
|
+
If the property is a diagonal rank-2 tensor, use the "vector" argument.
|
|
214
|
+
|
|
215
|
+
If the property is a full rank-2 tensor, use the "matrix" argument.
|
|
216
|
+
|
|
217
|
+
The max_value property must be set to tell EMerge how height this value can get
|
|
218
|
+
as it will be used to define the discretization of the mesh.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
max_value (float): The heighest value of the material property
|
|
222
|
+
scalar (Callable | None, optional): The scalar value function returning a float/complex. Defaults to None.
|
|
223
|
+
vector (Callable | None, optional): The diagonal rank-2 tensor function returning a (3,) array. Defaults to None.
|
|
224
|
+
matrix (Callable | None, optional): The rank-2 tensor function returning a (3,3) array. Defaults to None.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
_type_: _description_
|
|
228
|
+
"""
|
|
229
|
+
if scalar is not None:
|
|
230
|
+
def _func(f, x, y, z) -> np.ndarray:
|
|
231
|
+
return np.eye(3)[:, :, None] * scalar(f,x,y,z)[None, None, :]
|
|
232
|
+
if vector is not None:
|
|
233
|
+
def _func(f,x, y, z) -> np.ndarray:
|
|
234
|
+
N = x.shape[0]
|
|
235
|
+
out = np.zeros((3, 3, N), dtype=vector(1e9,0,0,0).dtype)
|
|
236
|
+
idx = np.arange(3)
|
|
237
|
+
out[idx, idx, :] = vector(f,x,y,z)
|
|
238
|
+
return out
|
|
239
|
+
if matrix is not None:
|
|
240
|
+
_func = matrix
|
|
241
|
+
|
|
242
|
+
self._func: Callable = _func
|
|
243
|
+
|
|
244
|
+
self._apply_to: np.ndarray = np.array([], dtype=np.int64)
|
|
245
|
+
self._x: np.ndarray = np.array([], dtype=np.float64)
|
|
246
|
+
self._y: np.ndarray = np.array([], dtype=np.float64)
|
|
247
|
+
self._z: np.ndarray = np.array([], dtype=np.float64)
|
|
248
|
+
|
|
249
|
+
self._fmax: Callable = lambda f: max_value
|
|
250
|
+
|
|
251
|
+
def initialize(self, x: np.ndarray, y: np.ndarray, z: np.ndarray, ids: np.ndarray) -> None:
|
|
252
|
+
self._apply_to = np.concatenate([self._apply_to, ids])
|
|
253
|
+
self._x = np.concatenate([self._x, x])
|
|
254
|
+
self._y = np.concatenate([self._y, y])
|
|
255
|
+
self._z = np.concatenate([self._z, z])
|
|
256
|
+
|
|
257
|
+
def __call__(self, f: float, data: np.ndarray) -> np.ndarray:
|
|
258
|
+
data[:,:,self._apply_to] = self._func(f,self._x, self._y,self._z)
|
|
259
|
+
return data
|
|
260
|
+
|
|
261
|
+
def scalar(self, f: float):
|
|
262
|
+
return self._func(f, 0,0,0)[0,0]
|
|
263
|
+
|
|
264
|
+
# To be finished once its clear how to deal with default values for functions
|
|
265
|
+
|
|
266
|
+
# def parse_material_property(value: complex | float | Callable):
|
|
267
|
+
# if not isinstance(value, Callable):
|
|
268
|
+
# return MatProperty(value)
|
|
269
|
+
# pass
|
|
21
270
|
|
|
22
|
-
@dataclass
|
|
23
271
|
class Material:
|
|
24
272
|
"""The Material class generalizes a material in the EMerge FEM environment.
|
|
25
273
|
|
|
26
274
|
If a scalar value is provided for the relative permittivity or the relative permeability
|
|
27
275
|
it will be used as multiplication entries for the material property diadic as identity matrix.
|
|
28
276
|
|
|
29
|
-
Additionally, a
|
|
30
|
-
|
|
31
|
-
|
|
277
|
+
Additionally, a frequency, coordinate or both frequency and coordinate dependent material property
|
|
278
|
+
may be supplied for the properties: er, ur, tand and cond.
|
|
279
|
+
|
|
280
|
+
To supply a frequency-dependent property use: emerge.FreqDependent()
|
|
281
|
+
To supply a coordinate-dependent property use: emerge.CoordDependent()
|
|
282
|
+
to supply a frequency and coordinate dependent property use: emerge.FreqCoordDependent()
|
|
32
283
|
|
|
33
284
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
_color_rgb: tuple[float,float,float] = (0.5, 0.5, 0.5)
|
|
43
|
-
opacity: float = 1.0
|
|
44
|
-
|
|
45
|
-
def __post_init__(self):
|
|
46
|
-
hex_str = self.color.lstrip('#')
|
|
47
|
-
self._color_rgb = tuple(int(hex_str[i:i+2], 16)/255.0 for i in (0, 2, 4))
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def sigma(self) -> float:
|
|
51
|
-
return self.cond
|
|
285
|
+
def __init__(self,
|
|
286
|
+
er: float | complex | np.ndarray | MatProperty = 1.0,
|
|
287
|
+
ur: float | complex | np.ndarray | MatProperty = 1.0,
|
|
288
|
+
tand: float | MatProperty = 0.0,
|
|
289
|
+
cond: float | MatProperty = 0.0,
|
|
290
|
+
_neff: float | None = None,
|
|
291
|
+
color: str ="#BEBEBE",
|
|
292
|
+
opacity: float = 1.0):
|
|
52
293
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if isinstance(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
294
|
+
if not isinstance(er, MatProperty):
|
|
295
|
+
er = MatProperty(er)
|
|
296
|
+
if not isinstance(ur, MatProperty):
|
|
297
|
+
ur = MatProperty(ur)
|
|
298
|
+
if not isinstance(tand, MatProperty):
|
|
299
|
+
tand = MatProperty(tand)
|
|
300
|
+
if not isinstance(cond, MatProperty):
|
|
301
|
+
cond = MatProperty(cond)
|
|
302
|
+
|
|
303
|
+
self.er: MatProperty = er
|
|
304
|
+
self.ur: MatProperty = ur
|
|
305
|
+
self.tand: MatProperty = tand
|
|
306
|
+
self.cond: MatProperty = cond
|
|
307
|
+
|
|
308
|
+
self.color: str = color
|
|
309
|
+
self.opacity: float = opacity
|
|
310
|
+
if _neff is None:
|
|
311
|
+
self._neff: Callable = lambda f: np.sqrt(self.ur._fmax(f)*self.er._fmax(f))
|
|
68
312
|
else:
|
|
69
|
-
|
|
313
|
+
self._neff: Callable = lambda f: _neff
|
|
314
|
+
hex_str = self.color.lstrip('#')
|
|
315
|
+
self._color_rgb = tuple(int(hex_str[i:i+2], 16)/255.0 for i in (0, 2, 4))
|
|
70
316
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return self._neff
|
|
75
|
-
er = self.ermat[0,0]
|
|
76
|
-
ur = self.urmat[0,0]
|
|
317
|
+
def initialize(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray, ids: np.ndarray):
|
|
318
|
+
"""Initializes the Material properties to be evaluated at xyz-coordinates for
|
|
319
|
+
a given set of tetrahedral ids.
|
|
77
320
|
|
|
78
|
-
|
|
321
|
+
Args:
|
|
322
|
+
xs (np.ndarray): The tet-centroid x-coordinates
|
|
323
|
+
ys (np.ndarray): The tet-centroid y-coordinates
|
|
324
|
+
zs (np.ndarray): The tet-centroid z-coordinates
|
|
325
|
+
ids (np.ndarray): The tet-indices
|
|
326
|
+
"""
|
|
327
|
+
self.er.initialize(xs, ys, zs, ids)
|
|
328
|
+
self.ur.initialize(xs, ys, zs, ids)
|
|
329
|
+
self.tand.initialize(xs, ys, zs, ids)
|
|
330
|
+
self.cond.initialize(xs, ys, zs, ids)
|
|
79
331
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
332
|
+
def reset(self) -> None:
|
|
333
|
+
"""Resets assignment of material properties to coordiantes and tetrahedral indices.
|
|
334
|
+
"""
|
|
335
|
+
self.er.reset()
|
|
336
|
+
self.ur.reset()
|
|
337
|
+
self.tand.reset()
|
|
338
|
+
self.cond.reset()
|
|
86
339
|
|
|
87
340
|
@property
|
|
88
|
-
def
|
|
89
|
-
|
|
341
|
+
def frequency_dependent(self) -> bool:
|
|
342
|
+
"""If The material property are at all frequency dependent.
|
|
90
343
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def fer3d(self) -> Callable:
|
|
96
|
-
if self._fer is None:
|
|
97
|
-
return lambda x,y,z: self.er*(1-1j*self.tand)*np.ones_like(x)
|
|
98
|
-
else:
|
|
99
|
-
return self._fer
|
|
344
|
+
Returns:
|
|
345
|
+
bool: _description_
|
|
346
|
+
"""
|
|
347
|
+
return self.er._freq_dependent or self.ur._freq_dependent or self.tand._freq_dependent or self.cond._freq_dependent
|
|
100
348
|
|
|
101
349
|
@property
|
|
102
|
-
def
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
350
|
+
def coordinate_dependent(self) -> bool:
|
|
351
|
+
"""If the material properties are at all coordinate dependent
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
bool: _description_
|
|
355
|
+
"""
|
|
356
|
+
return self.er._coord_dependent or self.ur._coord_dependent or self.tand._coord_dependent or self.cond._coord_dependent
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def neff(self, f: float):
|
|
360
|
+
""" Computes the maximum occuring effective refractive index for this material."""
|
|
361
|
+
return self._neff(f)
|
|
114
362
|
|
|
115
363
|
@property
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
else:
|
|
120
|
-
return self._fur
|
|
121
|
-
|
|
364
|
+
def color_rgb(self) -> tuple[float,float,float]:
|
|
365
|
+
return self._color_rgb
|
|
366
|
+
|
|
122
367
|
AIR = Material(color="#4496f3", opacity=0.05)
|
|
123
368
|
COPPER = Material(cond=5.8e7, color="#62290c")
|
emerge/_emerge/mesh3d.py
CHANGED
|
@@ -28,6 +28,7 @@ from loguru import logger
|
|
|
28
28
|
from functools import cache
|
|
29
29
|
from .bc import Periodic
|
|
30
30
|
from .mth.pairing import pair_coordinates
|
|
31
|
+
from .material import Material
|
|
31
32
|
|
|
32
33
|
@njit(f8(f8[:], f8[:], f8[:]), cache=True, nogil=True)
|
|
33
34
|
def area(x1: np.ndarray, x2: np.ndarray, x3: np.ndarray):
|
|
@@ -538,10 +539,14 @@ class Mesh3D(Mesh):
|
|
|
538
539
|
return conv_map, np.array(node_ids_2_unsorted), np.array(node_ids_2_sorted)
|
|
539
540
|
|
|
540
541
|
|
|
541
|
-
def retreive(self,
|
|
542
|
+
def retreive(self, volumes: list[GeoVolume]) -> list[Material]:
|
|
542
543
|
'''Retrieve the material properties of the geometry'''
|
|
543
|
-
arry = np.zeros((3,3,self.n_tets,), dtype=np.complex128)
|
|
544
|
-
|
|
544
|
+
#arry = np.zeros((3,3,self.n_tets,), dtype=np.complex128)
|
|
545
|
+
for vol in volumes:
|
|
546
|
+
vol.material.reset()
|
|
547
|
+
|
|
548
|
+
materials = []
|
|
549
|
+
|
|
545
550
|
xs = self.centers[0,:]
|
|
546
551
|
ys = self.centers[1,:]
|
|
547
552
|
zs = self.centers[2,:]
|
|
@@ -551,11 +556,12 @@ class Mesh3D(Mesh):
|
|
|
551
556
|
for dimtag in volume.dimtags:
|
|
552
557
|
etype, etag_list, ntags = gmsh.model.mesh.get_elements(*dimtag)
|
|
553
558
|
for etags in etag_list:
|
|
554
|
-
tet_ids = [self.tet_t2i[t] for t in etags]
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
+
tet_ids = np.array([self.tet_t2i[t] for t in etags])
|
|
560
|
+
volume.material.initialize(xs[tet_ids], ys[tet_ids], zs[tet_ids], tet_ids)
|
|
561
|
+
if volume.material not in materials:
|
|
562
|
+
materials.append(volume.material)
|
|
563
|
+
|
|
564
|
+
return materials
|
|
559
565
|
|
|
560
566
|
def plot_gmsh(self) -> None:
|
|
561
567
|
gmsh.fltk.run()
|
|
@@ -21,7 +21,7 @@ from ....elements.nedelec2 import Nedelec2
|
|
|
21
21
|
from ....elements.nedleg2 import NedelecLegrange2
|
|
22
22
|
from ....mth.optimized import gaus_quad_tri
|
|
23
23
|
from ....mth.pairing import pair_coordinates
|
|
24
|
-
|
|
24
|
+
from ....material import Material
|
|
25
25
|
from scipy.sparse import csr_matrix
|
|
26
26
|
from loguru import logger
|
|
27
27
|
from ..simjob import SimJob
|
|
@@ -188,9 +188,7 @@ class Assembler:
|
|
|
188
188
|
return E, B, np.array(solve_ids), nedlegfield
|
|
189
189
|
|
|
190
190
|
def assemble_freq_matrix(self, field: Nedelec2,
|
|
191
|
-
|
|
192
|
-
ur: np.ndarray,
|
|
193
|
-
sig: np.ndarray,
|
|
191
|
+
materials: list[Material],
|
|
194
192
|
bcs: list[BoundaryCondition],
|
|
195
193
|
frequency: float,
|
|
196
194
|
cache_matrices: bool = False) -> SimJob:
|
|
@@ -215,11 +213,29 @@ class Assembler:
|
|
|
215
213
|
# PREDEFINE CONSTANTS
|
|
216
214
|
W0 = 2*np.pi*frequency
|
|
217
215
|
K0 = W0/C0
|
|
218
|
-
|
|
216
|
+
|
|
217
|
+
is_frequency_dependent = False
|
|
219
218
|
mesh = field.mesh
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
|
|
220
|
+
for mat in materials:
|
|
221
|
+
if mat.frequency_dependent:
|
|
222
|
+
is_frequency_dependent = True
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
er = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
226
|
+
tand = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
227
|
+
cond = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
228
|
+
ur = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
229
|
+
|
|
230
|
+
for mat in materials:
|
|
231
|
+
er = mat.er(frequency, er)
|
|
232
|
+
ur = mat.ur(frequency, ur)
|
|
233
|
+
tand = mat.tand(frequency, tand)
|
|
234
|
+
cond = mat.cond(frequency, cond)
|
|
222
235
|
|
|
236
|
+
er = er*(1-1j*tand) - 1j*cond/(W0*EPS0)
|
|
237
|
+
|
|
238
|
+
is_frequency_dependent = is_frequency_dependent or np.any((cond > 0) & (cond < self.conductivity_limit)) # type: ignore
|
|
223
239
|
|
|
224
240
|
if cache_matrices and not is_frequency_dependent and self.cached_matrices is not None:
|
|
225
241
|
# IF CACHED AND AVAILABLE PULL E AND B FROM CACHE
|
|
@@ -253,10 +269,11 @@ class Assembler:
|
|
|
253
269
|
|
|
254
270
|
logger.debug('Implementing PEC Boundary Conditions.')
|
|
255
271
|
pec_ids: list[int] = []
|
|
272
|
+
|
|
256
273
|
# Conductivity above al imit, consider it all PEC
|
|
257
274
|
ipec = 0
|
|
258
275
|
for itet in range(field.n_tets):
|
|
259
|
-
if
|
|
276
|
+
if cond[0,0,itet] > self.conductivity_limit:
|
|
260
277
|
ipec+=1
|
|
261
278
|
pec_ids.extend(field.tet_to_field[:,itet])
|
|
262
279
|
if ipec>0:
|
|
@@ -388,12 +405,10 @@ class Assembler:
|
|
|
388
405
|
simjob.Pd = Pmat.getH()
|
|
389
406
|
simjob.has_periodic = has_periodic
|
|
390
407
|
|
|
391
|
-
return simjob
|
|
408
|
+
return simjob, (er, ur, cond)
|
|
392
409
|
|
|
393
410
|
def assemble_eig_matrix(self, field: Nedelec2,
|
|
394
|
-
|
|
395
|
-
ur: np.ndarray,
|
|
396
|
-
sig: np.ndarray,
|
|
411
|
+
materials: list[Material],
|
|
397
412
|
bcs: list[BoundaryCondition],
|
|
398
413
|
frequency: float) -> SimJob:
|
|
399
414
|
"""Assembles the eigenmode analysis matrix
|
|
@@ -420,9 +435,21 @@ class Assembler:
|
|
|
420
435
|
w0 = 2*np.pi*frequency
|
|
421
436
|
k0 = w0/C0
|
|
422
437
|
|
|
423
|
-
er =
|
|
438
|
+
er = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
439
|
+
tand = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
440
|
+
cond = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
441
|
+
ur = np.zeros((3,3,field.mesh.n_tets), dtype=np.complex128)
|
|
442
|
+
|
|
443
|
+
for mat in materials:
|
|
444
|
+
er = mat.er(frequency, er)
|
|
445
|
+
ur = mat.ur(frequency, ur)
|
|
446
|
+
tand = mat.tand(frequency, tand)
|
|
447
|
+
cond = mat.cond(frequency, cond)
|
|
448
|
+
|
|
449
|
+
er = er*(1-1j*tand) - 1j*cond/(w0*EPS0)
|
|
424
450
|
|
|
425
451
|
logger.debug('Assembling matrices')
|
|
452
|
+
|
|
426
453
|
E, B = tet_mass_stiffness_matrices(field, er, ur)
|
|
427
454
|
self.cached_matrices = (E, B)
|
|
428
455
|
|
|
@@ -439,7 +466,7 @@ class Assembler:
|
|
|
439
466
|
|
|
440
467
|
# Conductivity above a limit, consider it all PEC
|
|
441
468
|
for itet in range(field.n_tets):
|
|
442
|
-
if
|
|
469
|
+
if cond[0,0,itet] > self.conductivity_limit:
|
|
443
470
|
pec_ids.extend(field.tet_to_field[:,itet])
|
|
444
471
|
|
|
445
472
|
# PEC Boundary conditions
|
|
@@ -461,11 +488,7 @@ class Assembler:
|
|
|
461
488
|
# Robin BCs
|
|
462
489
|
if len(robin_bcs) > 0:
|
|
463
490
|
logger.debug('Implementing Robin Boundary Conditions.')
|
|
464
|
-
|
|
465
|
-
if len(robin_bcs) > 0:
|
|
466
|
-
logger.debug('Implementing Robin Boundary Conditions.')
|
|
467
|
-
|
|
468
|
-
gauss_points = gaus_quad_tri(4)
|
|
491
|
+
|
|
469
492
|
Bempty = field.empty_tri_matrix()
|
|
470
493
|
for bc in robin_bcs:
|
|
471
494
|
|
|
@@ -550,4 +573,4 @@ class Assembler:
|
|
|
550
573
|
simjob.Pd = Pmat.getH()
|
|
551
574
|
simjob.has_periodic = has_periodic
|
|
552
575
|
|
|
553
|
-
return simjob
|
|
576
|
+
return simjob, (er, ur, cond)
|