emerge 0.4.6__py3-none-any.whl → 0.4.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 +54 -0
- emerge/__main__.py +5 -0
- emerge/_emerge/__init__.py +42 -0
- emerge/_emerge/bc.py +197 -0
- emerge/_emerge/coord.py +119 -0
- emerge/_emerge/cs.py +523 -0
- emerge/_emerge/dataset.py +36 -0
- emerge/_emerge/elements/__init__.py +19 -0
- emerge/_emerge/elements/femdata.py +212 -0
- emerge/_emerge/elements/index_interp.py +64 -0
- emerge/_emerge/elements/legrange2.py +172 -0
- emerge/_emerge/elements/ned2_interp.py +645 -0
- emerge/_emerge/elements/nedelec2.py +140 -0
- emerge/_emerge/elements/nedleg2.py +217 -0
- emerge/_emerge/geo/__init__.py +24 -0
- emerge/_emerge/geo/horn.py +107 -0
- emerge/_emerge/geo/modeler.py +449 -0
- emerge/_emerge/geo/operations.py +254 -0
- emerge/_emerge/geo/pcb.py +1244 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
- emerge/_emerge/geo/pcb_tools/macro.py +79 -0
- emerge/_emerge/geo/pmlbox.py +204 -0
- emerge/_emerge/geo/polybased.py +529 -0
- emerge/_emerge/geo/shapes.py +427 -0
- emerge/_emerge/geo/step.py +77 -0
- emerge/_emerge/geo2d.py +86 -0
- emerge/_emerge/geometry.py +510 -0
- emerge/_emerge/howto.py +214 -0
- emerge/_emerge/logsettings.py +5 -0
- emerge/_emerge/material.py +118 -0
- emerge/_emerge/mesh3d.py +730 -0
- emerge/_emerge/mesher.py +339 -0
- emerge/_emerge/mth/common_functions.py +33 -0
- emerge/_emerge/mth/integrals.py +71 -0
- emerge/_emerge/mth/optimized.py +357 -0
- emerge/_emerge/periodic.py +263 -0
- emerge/_emerge/physics/__init__.py +0 -0
- emerge/_emerge/physics/microwave/__init__.py +1 -0
- emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
- emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
- emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
- emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
- emerge/_emerge/physics/microwave/periodic.py +82 -0
- emerge/_emerge/physics/microwave/port_functions.py +53 -0
- emerge/_emerge/physics/microwave/sc.py +175 -0
- emerge/_emerge/physics/microwave/simjob.py +147 -0
- emerge/_emerge/physics/microwave/sparam.py +138 -0
- emerge/_emerge/physics/microwave/touchstone.py +140 -0
- emerge/_emerge/plot/__init__.py +0 -0
- emerge/_emerge/plot/display.py +394 -0
- emerge/_emerge/plot/grapher.py +93 -0
- emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
- emerge/_emerge/plot/pyvista/__init__.py +1 -0
- emerge/_emerge/plot/pyvista/display.py +931 -0
- emerge/_emerge/plot/pyvista/display_settings.py +24 -0
- emerge/_emerge/plot/simple_plots.py +551 -0
- emerge/_emerge/plot.py +225 -0
- emerge/_emerge/projects/__init__.py +0 -0
- emerge/_emerge/projects/_gen_base.txt +32 -0
- emerge/_emerge/projects/_load_base.txt +24 -0
- emerge/_emerge/projects/generate_project.py +40 -0
- emerge/_emerge/selection.py +596 -0
- emerge/_emerge/simmodel.py +444 -0
- emerge/_emerge/simulation_data.py +411 -0
- emerge/_emerge/solver.py +993 -0
- emerge/_emerge/system.py +54 -0
- emerge/cli.py +19 -0
- emerge/lib.py +57 -0
- emerge/plot.py +1 -0
- emerge/pyvista.py +1 -0
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
- emerge-0.4.8.dist-info/RECORD +78 -0
- emerge-0.4.8.dist-info/entry_points.txt +2 -0
- emerge-0.4.6.dist-info/RECORD +0 -4
- emerge-0.4.6.dist-info/entry_points.txt +0 -2
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# EMerge is an open source Python based FEM EM simulation module.
|
|
2
|
+
# Copyright (C) 2025 Robert Fennis.
|
|
3
|
+
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see
|
|
16
|
+
# <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
from ...mesh3d import Mesh3D, SurfaceMesh
|
|
19
|
+
from ...geometry import GeoObject, GeoSurface, GeoVolume
|
|
20
|
+
from ...selection import FaceSelection, DomainSelection, EdgeSelection, Selection
|
|
21
|
+
from ...bc import PortBC
|
|
22
|
+
import numpy as np
|
|
23
|
+
from typing import Iterable, Literal, Callable
|
|
24
|
+
from functools import wraps
|
|
25
|
+
from ..display import BaseDisplay
|
|
26
|
+
from matplotlib.colors import Normalize
|
|
27
|
+
from matplotlib import cm
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
|
|
30
|
+
import matplotlib.pyplot as plt
|
|
31
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
32
|
+
|
|
33
|
+
def align_triangle_normals(nodes: np.ndarray, triangles: np.ndarray):
|
|
34
|
+
"""
|
|
35
|
+
Flip triangle winding to align with given normals.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
- nodes: (3, N) array of 3D points.
|
|
39
|
+
- triangles: (3, M) array of indices into nodes.
|
|
40
|
+
- normals: (3, M) array of desired triangle normals.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
- triangles_aligned: (3, M) array with consistent winding.
|
|
44
|
+
"""
|
|
45
|
+
# Get triangle vertices
|
|
46
|
+
p0 = nodes[:, triangles[0,:]]
|
|
47
|
+
p1 = nodes[:, triangles[1,:]]
|
|
48
|
+
p2 = nodes[:, triangles[2,:]]
|
|
49
|
+
|
|
50
|
+
# Compute current normals (not normalized)
|
|
51
|
+
ns = np.cross(p1 - p0, p2 - p0, axis=0)
|
|
52
|
+
tris_out = np.zeros_like(triangles, dtype=np.int64)
|
|
53
|
+
for it in range(triangles.shape[1]):
|
|
54
|
+
if (-ns[0,it]-ns[1,it]-ns[2,it]) < 0:
|
|
55
|
+
tris_out[:,it] = triangles[:,it]
|
|
56
|
+
else:
|
|
57
|
+
tris_out[:,it] = triangles[(0,2,1),it]
|
|
58
|
+
return tris_out
|
|
59
|
+
|
|
60
|
+
def _logscale(dx, dy, dz):
|
|
61
|
+
"""
|
|
62
|
+
Logarithmically scales vector magnitudes so that the largest remains unchanged
|
|
63
|
+
and others are scaled down logarithmically.
|
|
64
|
+
|
|
65
|
+
Parameters:
|
|
66
|
+
dx, dy, dz (np.ndarray): Components of vectors.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]: Scaled dx, dy, dz arrays.
|
|
70
|
+
"""
|
|
71
|
+
dx = np.asarray(dx)
|
|
72
|
+
dy = np.asarray(dy)
|
|
73
|
+
dz = np.asarray(dz)
|
|
74
|
+
|
|
75
|
+
# Compute original magnitudes
|
|
76
|
+
mags = np.sqrt(dx**2 + dy**2 + dz**2)
|
|
77
|
+
mags_nonzero = np.where(mags == 0, 1e-10, mags) # avoid log(0)
|
|
78
|
+
|
|
79
|
+
# Logarithmic scaling (scaled to max = original max)
|
|
80
|
+
log_mags = np.log10(mags_nonzero)
|
|
81
|
+
log_min = np.min(log_mags)
|
|
82
|
+
log_max = np.max(log_mags)
|
|
83
|
+
|
|
84
|
+
if log_max == log_min:
|
|
85
|
+
# All vectors have the same length
|
|
86
|
+
return dx, dy, dz
|
|
87
|
+
|
|
88
|
+
# Normalize log magnitudes to [0, 1]
|
|
89
|
+
log_scaled = (log_mags - log_min) / (log_max - log_min)
|
|
90
|
+
|
|
91
|
+
# Scale back to original max magnitude
|
|
92
|
+
max_mag = np.max(mags)
|
|
93
|
+
new_mags = log_scaled * max_mag
|
|
94
|
+
|
|
95
|
+
# Compute unit vectors
|
|
96
|
+
unit_dx = dx / mags_nonzero
|
|
97
|
+
unit_dy = dy / mags_nonzero
|
|
98
|
+
unit_dz = dz / mags_nonzero
|
|
99
|
+
|
|
100
|
+
# Apply scaled magnitudes
|
|
101
|
+
scaled_dx = unit_dx * new_mags
|
|
102
|
+
scaled_dy = unit_dy * new_mags
|
|
103
|
+
scaled_dz = unit_dz * new_mags
|
|
104
|
+
|
|
105
|
+
return scaled_dx, scaled_dy, scaled_dz
|
|
106
|
+
|
|
107
|
+
def _make_facecolors(C, cmap='viridis'):
|
|
108
|
+
"""
|
|
109
|
+
Convert C (N, M) to facecolors (N-1, M-1, 4) using a colormap.
|
|
110
|
+
"""
|
|
111
|
+
from matplotlib import cm
|
|
112
|
+
C_avg = 0.25 * (C[:-1, :-1] + C[1:, :-1] + C[:-1, 1:] + C[1:, 1:])
|
|
113
|
+
normed = (C_avg - np.min(C_avg)) / np.ptp(C_avg)
|
|
114
|
+
return plt.get_cmap(cmap)(normed)
|
|
115
|
+
|
|
116
|
+
def _min_distance(xs, ys, zs):
|
|
117
|
+
"""
|
|
118
|
+
Compute the minimum Euclidean distance between any two points
|
|
119
|
+
defined by the 1D arrays xs, ys, zs.
|
|
120
|
+
|
|
121
|
+
Parameters:
|
|
122
|
+
xs (np.ndarray): x-coordinates of the points
|
|
123
|
+
ys (np.ndarray): y-coordinates of the points
|
|
124
|
+
zs (np.ndarray): z-coordinates of the points
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
float: The minimum Euclidean distance between any two points
|
|
128
|
+
"""
|
|
129
|
+
# Stack the coordinates into a (N, 3) array
|
|
130
|
+
points = np.stack((xs, ys, zs), axis=-1)
|
|
131
|
+
|
|
132
|
+
# Compute pairwise squared distances using broadcasting
|
|
133
|
+
diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]
|
|
134
|
+
dists_squared = np.sum(diff ** 2, axis=-1)
|
|
135
|
+
|
|
136
|
+
# Set diagonal to infinity to ignore zero distances to self
|
|
137
|
+
np.fill_diagonal(dists_squared, np.inf)
|
|
138
|
+
|
|
139
|
+
# Get the minimum distance
|
|
140
|
+
min_dist = np.sqrt(np.min(dists_squared))
|
|
141
|
+
return min_dist
|
|
142
|
+
|
|
143
|
+
def _norm(x, y, z):
|
|
144
|
+
return np.sqrt(np.abs(x)**2 + np.abs(y)**2 + np.abs(z)**2)
|
|
145
|
+
|
|
146
|
+
def _select(obj: GeoObject | Selection) -> Selection:
|
|
147
|
+
if isinstance(obj, GeoObject):
|
|
148
|
+
return obj.select
|
|
149
|
+
return obj
|
|
150
|
+
|
|
151
|
+
def _merge(lst: list[GeoObject | Selection]) -> Selection:
|
|
152
|
+
selections = [_select(item) for item in lst]
|
|
153
|
+
dim = selections[0].dim
|
|
154
|
+
all_tags = []
|
|
155
|
+
for item in lst:
|
|
156
|
+
all_tags.extend(_select(item).tags)
|
|
157
|
+
|
|
158
|
+
if dim==1:
|
|
159
|
+
return EdgeSelection(all_tags)
|
|
160
|
+
elif dim==2:
|
|
161
|
+
return FaceSelection(all_tags)
|
|
162
|
+
elif dim==3:
|
|
163
|
+
return DomainSelection(all_tags)
|
|
164
|
+
|
|
165
|
+
class MPLDisplay(BaseDisplay):
|
|
166
|
+
COLORS: dict = {'green': (0,0.8,0), 'red': (0.8,0,0), 'blue': (0,0,0.8)}
|
|
167
|
+
def __init__(self, mesh: Mesh3D):
|
|
168
|
+
self._mesh: Mesh3D = mesh
|
|
169
|
+
self._fig = None
|
|
170
|
+
self._ax = None
|
|
171
|
+
|
|
172
|
+
def init(self):
|
|
173
|
+
if self._fig is None or self._ax is None:
|
|
174
|
+
self._fig = plt.figure()
|
|
175
|
+
self._ax = self._fig.add_subplot(111, projection='3d')
|
|
176
|
+
self._ax.axis('equal')
|
|
177
|
+
self._ax.set_aspect('equal')
|
|
178
|
+
|
|
179
|
+
def show(self):
|
|
180
|
+
plt.show()
|
|
181
|
+
self._fig = None
|
|
182
|
+
self._ax = None
|
|
183
|
+
|
|
184
|
+
## OBLIGATORY METHODS
|
|
185
|
+
def add_object(self, obj: GeoObject | Selection | Iterable, opacity: float = 1, color: str = None, **kwargs):
|
|
186
|
+
self.init()
|
|
187
|
+
|
|
188
|
+
if color is not None:
|
|
189
|
+
color = self.COLORS.get(color,(0,0.5,0))
|
|
190
|
+
|
|
191
|
+
boundary_tris = self._mesh.boundary_triangles(obj.dimtags)
|
|
192
|
+
tris = self._mesh.tris[:,boundary_tris]
|
|
193
|
+
tris = align_triangle_normals(self._mesh.nodes, tris)
|
|
194
|
+
x = self._mesh.nodes[0,:]
|
|
195
|
+
y = self._mesh.nodes[1,:]
|
|
196
|
+
z = self._mesh.nodes[2,:]
|
|
197
|
+
surf = self._ax.plot_trisurf(
|
|
198
|
+
x, y, z,triangles=tris.T,
|
|
199
|
+
color=obj.color_rgb + (opacity,),
|
|
200
|
+
linewidth=0.2,
|
|
201
|
+
antialiased=True,
|
|
202
|
+
shade=True
|
|
203
|
+
)
|
|
204
|
+
# Equal aspect ratio
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def add_scatter(self, xs: np.ndarray, ys: np.ndarray, zs: np.ndarray):
|
|
208
|
+
self.init()
|
|
209
|
+
self._ax.scatter(xs,ys,zs)
|
|
210
|
+
|
|
211
|
+
def add_portmode(self, port: PortBC, k0: float, Npoints: int = 5, dv=(0,0,0), XYZ=None,
|
|
212
|
+
field: Literal['E','H'] = 'E'):
|
|
213
|
+
if XYZ:
|
|
214
|
+
X,Y,Z = XYZ
|
|
215
|
+
else:
|
|
216
|
+
X,Y,Z = port.selection.sample(Npoints)
|
|
217
|
+
for x,y,z in zip(X,Y,Z):
|
|
218
|
+
self.add_portmode(port, k0, Npoints, dv, (x,y,z), field)
|
|
219
|
+
return
|
|
220
|
+
X = X+dv[0]
|
|
221
|
+
Y = Y+dv[1]
|
|
222
|
+
Z = Z+dv[2]
|
|
223
|
+
xf = X.flatten()
|
|
224
|
+
yf = Y.flatten()
|
|
225
|
+
zf = Z.flatten()
|
|
226
|
+
|
|
227
|
+
d1 = np.sqrt((X[0,1]-X[0,0])**2 + (Y[0,1]-Y[0,0])**2 + (Z[0,1]-Z[0,0])**2)
|
|
228
|
+
d2 = np.sqrt((X[1,0]-X[0,0])**2 + (Y[1,0]-Y[0,0])**2 + (Z[1,0]-Z[0,0])**2)
|
|
229
|
+
d = min(d1, d2)
|
|
230
|
+
|
|
231
|
+
F = port.port_mode_3d_global(xf,yf,zf, k0, which=field)
|
|
232
|
+
|
|
233
|
+
Fx = F[0,:].reshape(X.shape)
|
|
234
|
+
Fy = F[1,:].reshape(X.shape)
|
|
235
|
+
Fz = F[2,:].reshape(X.shape)
|
|
236
|
+
|
|
237
|
+
if field=='H':
|
|
238
|
+
F = np.imag(F)
|
|
239
|
+
Fnorm = np.sqrt(Fx.imag**2 + Fy.imag**2 + Fz.imag**2)
|
|
240
|
+
else:
|
|
241
|
+
F = np.real(F)
|
|
242
|
+
Fnorm = np.sqrt(Fx.real**2 + Fy.real**2 + Fz.real**2)
|
|
243
|
+
|
|
244
|
+
cmap = 'viridis'
|
|
245
|
+
|
|
246
|
+
colors = _make_facecolors(Fnorm)
|
|
247
|
+
surf = self._ax.plot_surface(X, Y, Z,facecolors=colors,
|
|
248
|
+
rstride=1, cstride=1, antialiased=True, linewidth=0)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
N = np.max(Fnorm.flatten())*d*3
|
|
253
|
+
self.add_quiver(xf, yf, zf, F[0,:].real/N, F[1,:].real/N, F[2,:].real/N)
|
|
254
|
+
|
|
255
|
+
def add_quiver(self, x: np.ndarray, y: np.ndarray, z: np.ndarray,
|
|
256
|
+
dx: np.ndarray, dy: np.ndarray, dz: np.ndarray,
|
|
257
|
+
scale: float = 1,
|
|
258
|
+
scalemode: Literal['lin','log'] = 'lin'):
|
|
259
|
+
|
|
260
|
+
self.init()
|
|
261
|
+
dlmax = np.sqrt(dx**2+dy**2+dz**2)
|
|
262
|
+
dmin = _min_distance(x,y,z)
|
|
263
|
+
scale = dmin/max(dlmax)
|
|
264
|
+
self._ax.quiver(x,y,z,dx*scale,dy*scale,dz*scale, color='black')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .display import PVDisplay
|