emerge 0.6.7__py3-none-any.whl → 0.6.9__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.

Files changed (33) hide show
  1. emerge/__init__.py +2 -2
  2. emerge/_emerge/_cache_check.py +1 -1
  3. emerge/_emerge/elements/femdata.py +3 -2
  4. emerge/_emerge/elements/index_interp.py +1 -2
  5. emerge/_emerge/elements/ned2_interp.py +16 -16
  6. emerge/_emerge/elements/nedelec2.py +17 -6
  7. emerge/_emerge/elements/nedleg2.py +21 -9
  8. emerge/_emerge/geo/__init__.py +1 -1
  9. emerge/_emerge/geo/horn.py +0 -1
  10. emerge/_emerge/geo/modeler.py +1 -1
  11. emerge/_emerge/geo/operations.py +13 -0
  12. emerge/_emerge/geo/pcb_tools/calculator.py +2 -3
  13. emerge/_emerge/geo/pmlbox.py +35 -11
  14. emerge/_emerge/geometry.py +13 -6
  15. emerge/_emerge/material.py +334 -82
  16. emerge/_emerge/mesh3d.py +14 -8
  17. emerge/_emerge/physics/microwave/assembly/assembler.py +43 -20
  18. emerge/_emerge/physics/microwave/microwave_3d.py +57 -44
  19. emerge/_emerge/physics/microwave/microwave_bc.py +26 -24
  20. emerge/_emerge/physics/microwave/microwave_data.py +90 -7
  21. emerge/_emerge/plot/pyvista/display.py +53 -15
  22. emerge/_emerge/plot/pyvista/display_settings.py +4 -1
  23. emerge/_emerge/plot/simple_plots.py +42 -26
  24. emerge/_emerge/projects/_load_base.txt +1 -2
  25. emerge/_emerge/selection.py +4 -0
  26. emerge/_emerge/simmodel.py +21 -9
  27. emerge/_emerge/solver.py +45 -18
  28. emerge/lib.py +256 -250
  29. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/METADATA +2 -1
  30. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/RECORD +33 -33
  31. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/licenses/LICENSE +2 -2
  32. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/WHEEL +0 -0
  33. {emerge-0.6.7.dist-info → emerge-0.6.9.dist-info}/entry_points.txt +0 -0
@@ -16,108 +16,360 @@
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 function may be provided that computes a coordinate dependent material property
30
- for _fer. For example: Material(_fer = lambda x,y,z: ...).
31
- The x,y and z coordinates are provided as a (N,) np.ndarray. The return array must be of shape (3,3,N)!
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
- er: float = 1
35
- ur: float = 1
36
- tand: float = 0
37
- cond: float = 0
38
- _neff: float | None = None
39
- _fer: Callable | None= None
40
- _fur: Callable | None = None
41
- color: str = "#BEBEBE"
42
- _color_rgb: tuple[float,float,float] = (0.5, 0.5, 0.5)
43
- opacity: float = 1.0
44
-
45
- def __post_init__(self):
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,
293
+ _metal: bool = False,
294
+ name: str = 'unnamed'):
295
+
296
+ if not isinstance(er, MatProperty):
297
+ er = MatProperty(er)
298
+ if not isinstance(ur, MatProperty):
299
+ ur = MatProperty(ur)
300
+ if not isinstance(tand, MatProperty):
301
+ tand = MatProperty(tand)
302
+ if not isinstance(cond, MatProperty):
303
+ cond = MatProperty(cond)
304
+
305
+ self.name: str = name
306
+ self.er: MatProperty = er
307
+ self.ur: MatProperty = ur
308
+ self.tand: MatProperty = tand
309
+ self.cond: MatProperty = cond
310
+
311
+ self.color: str = color
312
+ self.opacity: float = opacity
313
+ if _neff is None:
314
+ self._neff: Callable = lambda f: np.sqrt(self.ur._fmax(f)*self.er._fmax(f))
315
+ else:
316
+ self._neff: Callable = lambda f: _neff
46
317
  hex_str = self.color.lstrip('#')
47
318
  self._color_rgb = tuple(int(hex_str[i:i+2], 16)/255.0 for i in (0, 2, 4))
319
+ self._metal: bool = _metal
48
320
 
49
- @property
50
- def sigma(self) -> float:
51
- return self.cond
52
-
53
- @property
54
- def color_rgb(self) -> tuple[float,float,float]:
55
- return self._color_rgb
321
+ def __str__(self) -> str:
322
+ return f'Material({self.name})'
56
323
 
57
- @property
58
- def ermat(self) -> np.ndarray:
59
- if isinstance(self.er, (float, complex, int, np.float64, np.complex128)):
60
- return self.er*(1-1j*self.tand)*np.eye(3)
61
- else:
62
- return self.er*(1-1j*self.tand)
63
-
64
- @property
65
- def urmat(self) -> np.ndarray:
66
- if isinstance(self.ur, (float, complex, int, np.float64, np.complex128)):
67
- return self.ur*np.eye(3)
68
- else:
69
- return self.ur
70
-
71
- @property
72
- def neff(self) -> complex:
73
- if self._neff is not None:
74
- return self._neff
75
- er = self.ermat[0,0]
76
- ur = self.urmat[0,0]
324
+ def initialize(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray, ids: np.ndarray):
325
+ """Initializes the Material properties to be evaluated at xyz-coordinates for
326
+ a given set of tetrahedral ids.
77
327
 
78
- return np.abs(np.sqrt(er*(1-1j*self.tand)*ur))
328
+ Args:
329
+ xs (np.ndarray): The tet-centroid x-coordinates
330
+ ys (np.ndarray): The tet-centroid y-coordinates
331
+ zs (np.ndarray): The tet-centroid z-coordinates
332
+ ids (np.ndarray): The tet-indices
333
+ """
334
+ self.er.initialize(xs, ys, zs, ids)
335
+ self.ur.initialize(xs, ys, zs, ids)
336
+ self.tand.initialize(xs, ys, zs, ids)
337
+ self.cond.initialize(xs, ys, zs, ids)
79
338
 
80
- @property
81
- def fer2d(self) -> Callable:
82
- if self._fer is None:
83
- return lambda x,y: self.er*(1-1j*self.tand)*np.ones_like(x)
84
- else:
85
- return self._fer
339
+ def reset(self) -> None:
340
+ """Resets assignment of material properties to coordiantes and tetrahedral indices.
341
+ """
342
+ self.er.reset()
343
+ self.ur.reset()
344
+ self.tand.reset()
345
+ self.cond.reset()
86
346
 
87
347
  @property
88
- def fur2d(self) -> Callable:
89
- if self._fur is None:
348
+ def frequency_dependent(self) -> bool:
349
+ """If The material property are at all frequency dependent.
90
350
 
91
- return lambda x,y: self.ur*np.ones_like(x)
92
- else:
93
- return self._fur
94
- @property
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
351
+ Returns:
352
+ bool: _description_
353
+ """
354
+ return self.er._freq_dependent or self.ur._freq_dependent or self.tand._freq_dependent or self.cond._freq_dependent
100
355
 
101
356
  @property
102
- def fur3d(self) -> Callable:
103
- if self._fur is None:
104
- return lambda x,y,z: self.ur*np.ones_like(x)
105
- else:
106
- return self._fur
107
- @property
108
- def fer3d_mat(self) -> Callable:
109
- if self._fer is None:
110
-
111
- return lambda x,y,z: np.repeat(self.ermat[:, :, np.newaxis], x.shape[0], axis=2)
112
- else:
113
- return self._fer
357
+ def coordinate_dependent(self) -> bool:
358
+ """If the material properties are at all coordinate dependent
359
+
360
+ Returns:
361
+ bool: _description_
362
+ """
363
+ return self.er._coord_dependent or self.ur._coord_dependent or self.tand._coord_dependent or self.cond._coord_dependent
364
+
365
+
366
+ def neff(self, f: float):
367
+ """ Computes the maximum occuring effective refractive index for this material."""
368
+ return self._neff(f)
114
369
 
115
370
  @property
116
- def fur3d_mat(self) -> Callable:
117
- if self._fur is None:
118
- return lambda x,y,z: np.repeat(self.urmat[:, :, np.newaxis], x.shape[0], axis=2)
119
- else:
120
- return self._fur
121
-
122
- AIR = Material(color="#4496f3", opacity=0.05)
123
- COPPER = Material(cond=5.8e7, color="#62290c")
371
+ def color_rgb(self) -> tuple[float,float,float]:
372
+ return self._color_rgb
373
+
374
+ AIR = Material(color="#4496f3", opacity=0.05, name='Air')
375
+ COPPER = Material(cond=5.8e7, color="#62290c", _metal=True, name='Copper')
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, material_selector: Callable, volumes: list[GeoVolume]) -> np.ndarray:
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
- value = material_selector(volume.material, xs[tet_ids], ys[tet_ids], zs[tet_ids])
557
- arry[:,:,tet_ids] = value
558
- return arry
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
- er: np.ndarray,
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
- er = er - 1j*sig/(W0*EPS0)*np.repeat(np.eye(3)[:, :, np.newaxis], er.shape[2], axis=2)
221
- is_frequency_dependent: bool = np.any((sig > 0) & (sig < self.conductivity_limit)) # type: ignore
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 sig[itet] > self.conductivity_limit:
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
- er: np.ndarray,
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 = er - 1j*sig/(w0*EPS0)*np.repeat(np.eye(3)[:, :, np.newaxis], er.shape[2], axis=2)
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 sig[itet] > self.conductivity_limit:
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)