emerge 0.6.6__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 +44 -4
- emerge/_emerge/geo/pcb.py +44 -9
- emerge/_emerge/geo/pcb_tools/calculator.py +2 -3
- emerge/_emerge/geo/pmlbox.py +35 -11
- emerge/_emerge/geo/shapes.py +61 -26
- 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 +12 -8
- emerge/_emerge/physics/microwave/microwave_data.py +74 -5
- emerge/_emerge/plot/pyvista/display.py +132 -24
- emerge/_emerge/plot/simple_plots.py +1 -1
- emerge/_emerge/simmodel.py +22 -13
- emerge/_emerge/solver.py +49 -22
- {emerge-0.6.6.dist-info → emerge-0.6.8.dist-info}/METADATA +1 -1
- {emerge-0.6.6.dist-info → emerge-0.6.8.dist-info}/RECORD +31 -31
- {emerge-0.6.6.dist-info → emerge-0.6.8.dist-info}/licenses/LICENSE +2 -2
- {emerge-0.6.6.dist-info → emerge-0.6.8.dist-info}/WHEEL +0 -0
- {emerge-0.6.6.dist-info → emerge-0.6.8.dist-info}/entry_points.txt +0 -0
emerge/_emerge/geo/shapes.py
CHANGED
|
@@ -30,6 +30,7 @@ class Alignment(Enum):
|
|
|
30
30
|
CENTER = 1
|
|
31
31
|
CORNER = 2
|
|
32
32
|
|
|
33
|
+
|
|
33
34
|
class Box(GeoVolume):
|
|
34
35
|
"""Creates a box volume object.
|
|
35
36
|
Specify the alignment of the box with the provided position. The options are CORNER (default)
|
|
@@ -100,7 +101,7 @@ class Box(GeoVolume):
|
|
|
100
101
|
|
|
101
102
|
tags = list(reduce(lambda a,b: a+b, tagslist))
|
|
102
103
|
return FaceSelection(tags)
|
|
103
|
-
|
|
104
|
+
|
|
104
105
|
|
|
105
106
|
class Sphere(GeoVolume):
|
|
106
107
|
"""Generates a sphere objected centered ont he position with the given radius
|
|
@@ -122,6 +123,7 @@ class Sphere(GeoVolume):
|
|
|
122
123
|
x,y,z = position
|
|
123
124
|
self.tags: list[int] = [gmsh.model.occ.addSphere(x,y,z,radius),]
|
|
124
125
|
|
|
126
|
+
|
|
125
127
|
class XYPlate(GeoSurface):
|
|
126
128
|
"""Generates and XY-plane oriented plate
|
|
127
129
|
|
|
@@ -209,27 +211,43 @@ class Plate(GeoSurface):
|
|
|
209
211
|
tags: list[int] = [gmsh.model.occ.addPlaneSurface([tag_wire,]),]
|
|
210
212
|
super().__init__(tags)
|
|
211
213
|
|
|
214
|
+
|
|
212
215
|
class Cylinder(GeoVolume):
|
|
216
|
+
"""Generates a Cylinder object in 3D space.
|
|
217
|
+
The cylinder will always be placed in the origin of the provided CoordinateSystem.
|
|
218
|
+
The bottom cylinder plane is always placed in the XY-plane. The length of the cylinder is
|
|
219
|
+
oriented along the Z-axis.
|
|
213
220
|
|
|
221
|
+
By default the cylinder uses the Open Cascade modeling for a cylinder. In this representation
|
|
222
|
+
the surface of the cylinder is approximated with a tolerance thay may be irregular.
|
|
223
|
+
As an alternative, the argument Nsections may be provided in which case the Cylinder is replaced
|
|
224
|
+
by an extrusion of a regular N-sided polygon.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
radius (float): The radius of the Cylinder
|
|
228
|
+
height (float): The height of the Cylinder
|
|
229
|
+
cs (CoordinateSystem, optional): The coordinate system. Defaults to GCS.
|
|
230
|
+
Nsections (int, optional): The number of sections. Defaults to None.
|
|
231
|
+
"""
|
|
214
232
|
def __init__(self,
|
|
215
233
|
radius: float,
|
|
216
234
|
height: float,
|
|
217
|
-
cs: CoordinateSystem =
|
|
218
|
-
Nsections: int = None):
|
|
235
|
+
cs: CoordinateSystem = GCS,
|
|
236
|
+
Nsections: int | None = None):
|
|
219
237
|
"""Generates a Cylinder object in 3D space.
|
|
220
|
-
The
|
|
221
|
-
The bottom
|
|
238
|
+
The cylinder will always be placed in the origin of the provided CoordinateSystem.
|
|
239
|
+
The bottom cylinder plane is always placed in the XY-plane. The length of the cylinder is
|
|
222
240
|
oriented along the Z-axis.
|
|
223
241
|
|
|
224
|
-
By default the
|
|
225
|
-
the surface of the
|
|
242
|
+
By default the cylinder uses the Open Cascade modeling for a cylinder. In this representation
|
|
243
|
+
the surface of the cylinder is approximated with a tolerance thay may be irregular.
|
|
226
244
|
As an alternative, the argument Nsections may be provided in which case the Cylinder is replaced
|
|
227
245
|
by an extrusion of a regular N-sided polygon.
|
|
228
246
|
|
|
229
247
|
Args:
|
|
230
248
|
radius (float): The radius of the Cylinder
|
|
231
249
|
height (float): The height of the Cylinder
|
|
232
|
-
cs (CoordinateSystem, optional): The coordinate system. Defaults to
|
|
250
|
+
cs (CoordinateSystem, optional): The coordinate system. Defaults to GCS.
|
|
233
251
|
Nsections (int, optional): The number of sections. Defaults to None.
|
|
234
252
|
"""
|
|
235
253
|
ax = cs.zax.np
|
|
@@ -270,33 +288,46 @@ class Cylinder(GeoVolume):
|
|
|
270
288
|
xo, yo, zo = self.cs.in_global_cs(x.flatten(), y.flatten(), z.flatten())
|
|
271
289
|
return xo, yo, zo
|
|
272
290
|
|
|
291
|
+
|
|
273
292
|
class CoaxCylinder(GeoVolume):
|
|
274
|
-
"""
|
|
275
|
-
|
|
293
|
+
"""Generates a Coaxial cylinder object in 3D space.
|
|
294
|
+
The coaxial cylinder will always be placed in the origin of the provided CoordinateSystem.
|
|
295
|
+
The bottom coax plane is always placed in the XY-plane. The lenth of the coax is
|
|
296
|
+
oriented along the Z-axis.
|
|
297
|
+
|
|
298
|
+
By default the coax uses the Open Cascade modeling for a cylinder. In this representation
|
|
299
|
+
the surface of the cylinder is approximated with a tolerance thay may be irregular.
|
|
300
|
+
As an alternative, the argument Nsections may be provided in which case the Cylinder is replaced
|
|
301
|
+
by an extrusion of a regular N-sided polygon.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
radius (float): The radius of the Cylinder
|
|
305
|
+
height (float): The height of the Cylinder
|
|
306
|
+
cs (CoordinateSystem, optional): The coordinate system. Defaults to GCS.
|
|
307
|
+
Nsections (int, optional): The number of sections. Defaults to None.
|
|
308
|
+
"""
|
|
276
309
|
def __init__(self,
|
|
277
310
|
rout: float,
|
|
278
311
|
rin: float,
|
|
279
312
|
height: float,
|
|
280
|
-
cs: CoordinateSystem =
|
|
281
|
-
Nsections: int = None):
|
|
282
|
-
"""Generates a Coaxial
|
|
283
|
-
The coaxial
|
|
313
|
+
cs: CoordinateSystem = GCS,
|
|
314
|
+
Nsections: int | None = None):
|
|
315
|
+
"""Generates a Coaxial cylinder object in 3D space.
|
|
316
|
+
The coaxial cylinder will always be placed in the origin of the provided CoordinateSystem.
|
|
284
317
|
The bottom coax plane is always placed in the XY-plane. The lenth of the coax is
|
|
285
318
|
oriented along the Z-axis.
|
|
286
319
|
|
|
287
|
-
By default the coax uses the Open Cascade modeling for a
|
|
288
|
-
the surface of the
|
|
320
|
+
By default the coax uses the Open Cascade modeling for a cylinder. In this representation
|
|
321
|
+
the surface of the cylinder is approximated with a tolerance thay may be irregular.
|
|
289
322
|
As an alternative, the argument Nsections may be provided in which case the Cylinder is replaced
|
|
290
323
|
by an extrusion of a regular N-sided polygon.
|
|
291
324
|
|
|
292
325
|
Args:
|
|
293
326
|
radius (float): The radius of the Cylinder
|
|
294
327
|
height (float): The height of the Cylinder
|
|
295
|
-
cs (CoordinateSystem, optional): The coordinate system. Defaults to
|
|
328
|
+
cs (CoordinateSystem, optional): The coordinate system. Defaults to GCS.
|
|
296
329
|
Nsections (int, optional): The number of sections. Defaults to None.
|
|
297
330
|
"""
|
|
298
|
-
if cs is None:
|
|
299
|
-
cs = GCS
|
|
300
331
|
if rout <= rin:
|
|
301
332
|
raise ValueError("Outer radius must be greater than inner radius.")
|
|
302
333
|
|
|
@@ -336,7 +367,7 @@ class CoaxCylinder(GeoVolume):
|
|
|
336
367
|
return xo, yo, zo
|
|
337
368
|
|
|
338
369
|
class HalfSphere(GeoVolume):
|
|
339
|
-
|
|
370
|
+
"""A half sphere volume."""
|
|
340
371
|
def __init__(self,
|
|
341
372
|
radius: float,
|
|
342
373
|
position: tuple = (0,0,0),
|
|
@@ -362,8 +393,6 @@ class HalfSphere(GeoVolume):
|
|
|
362
393
|
self._add_face_pointer('back',np.array(position), np.array(direction))
|
|
363
394
|
self._add_face_pointer('bottom',np.array(position), np.array(direction))
|
|
364
395
|
self._add_face_pointer('face',np.array(position), np.array(direction))
|
|
365
|
-
|
|
366
|
-
|
|
367
396
|
|
|
368
397
|
|
|
369
398
|
class OldBox(GeoVolume):
|
|
@@ -389,8 +418,6 @@ class OldBox(GeoVolume):
|
|
|
389
418
|
alignment (Alignment, optional): Which point of the box is placed at the position.
|
|
390
419
|
Defaults to Alignment.CORNER.
|
|
391
420
|
"""
|
|
392
|
-
|
|
393
|
-
|
|
394
421
|
if alignment is Alignment.CORNER:
|
|
395
422
|
position = (position[0]+width/2, position[1]+depth/2, position[2])
|
|
396
423
|
elif alignment is Alignment.CENTER:
|
|
@@ -472,7 +499,6 @@ class OldBox(GeoVolume):
|
|
|
472
499
|
self._add_face_pointer('right', pc + width/2*wax, wax)
|
|
473
500
|
self._add_face_pointer('top', pc + height/2*hax, hax)
|
|
474
501
|
self._add_face_pointer('bottom', pc - height/2*hax, -hax)
|
|
475
|
-
|
|
476
502
|
|
|
477
503
|
def outside(self, *exclude: Literal['bottom','top','right','left','front','back']) -> FaceSelection:
|
|
478
504
|
"""Select all outside faces except for the once specified by outside
|
|
@@ -485,8 +511,17 @@ class OldBox(GeoVolume):
|
|
|
485
511
|
tags = list(reduce(lambda a,b: a+b, tagslist))
|
|
486
512
|
return FaceSelection(tags)
|
|
487
513
|
|
|
514
|
+
|
|
488
515
|
class Cone(GeoVolume):
|
|
489
|
-
|
|
516
|
+
"""Constructis a cone that starts at position p0 and is aimed in the given direction.
|
|
517
|
+
r1 is the start radius and r2 the end radius. The magnitude of direction determines its length.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
p0 (tuple[float, float, float]): _description_
|
|
521
|
+
direction (tuple[float, float, float]): _description_
|
|
522
|
+
r1 (float): _description_
|
|
523
|
+
r2 (float): _description_
|
|
524
|
+
"""
|
|
490
525
|
def __init__(self, p0: tuple[float, float, float],
|
|
491
526
|
direction: tuple[float, float, float],
|
|
492
527
|
r1: float,
|
emerge/_emerge/geometry.py
CHANGED
|
@@ -376,7 +376,7 @@ class GeoObject:
|
|
|
376
376
|
return self
|
|
377
377
|
|
|
378
378
|
def prio_up(self) -> GeoObject:
|
|
379
|
-
"""
|
|
379
|
+
"""Increases the material selection priority by 1
|
|
380
380
|
|
|
381
381
|
Returns:
|
|
382
382
|
GeoObject: _description_
|
|
@@ -385,7 +385,7 @@ class GeoObject:
|
|
|
385
385
|
return self
|
|
386
386
|
|
|
387
387
|
def prio_down(self) -> GeoObject:
|
|
388
|
-
"""
|
|
388
|
+
"""Decreases the material selection priority by 1
|
|
389
389
|
|
|
390
390
|
Returns:
|
|
391
391
|
GeoObject: _description_
|
|
@@ -394,7 +394,7 @@ class GeoObject:
|
|
|
394
394
|
return self
|
|
395
395
|
|
|
396
396
|
def background(self) -> GeoObject:
|
|
397
|
-
"""Set the priority to be on the background.
|
|
397
|
+
"""Set the material selection priority to be on the background.
|
|
398
398
|
|
|
399
399
|
Returns:
|
|
400
400
|
GeoObject: _description_
|
|
@@ -403,7 +403,7 @@ class GeoObject:
|
|
|
403
403
|
return self
|
|
404
404
|
|
|
405
405
|
def foreground(self) -> GeoObject:
|
|
406
|
-
"""Set the priority to be on top.
|
|
406
|
+
"""Set the material selection priority to be on top.
|
|
407
407
|
|
|
408
408
|
Returns:
|
|
409
409
|
GeoObject: _description_
|
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")
|