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.

@@ -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 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):
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
- @property
54
- def color_rgb(self) -> tuple[float,float,float]:
55
- return self._color_rgb
56
-
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)
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
- return self.ur
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
- @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]
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
- return np.abs(np.sqrt(er*(1-1j*self.tand)*ur))
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
- @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
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 fur2d(self) -> Callable:
89
- if self._fur is None:
341
+ def frequency_dependent(self) -> bool:
342
+ """If The material property are at all frequency dependent.
90
343
 
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
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 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
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 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
-
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, 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)