emsutil 0.2.2__tar.gz → 0.4.0__tar.gz

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.
Files changed (34) hide show
  1. {emsutil-0.2.2 → emsutil-0.4.0}/.bumpversion.toml +1 -1
  2. {emsutil-0.2.2 → emsutil-0.4.0}/PKG-INFO +3 -1
  3. emsutil-0.4.0/emerge-install-mumps +32 -0
  4. {emsutil-0.2.2 → emsutil-0.4.0}/pyproject.toml +3 -1
  5. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/__init__.py +2 -1
  6. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/emdata.py +31 -16
  7. emsutil-0.4.0/src/emsutil/file.py +192 -0
  8. emsutil-0.4.0/src/emsutil/inexport/ffdata.py +304 -0
  9. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/material.py +37 -26
  10. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/themes.py +58 -0
  11. {emsutil-0.2.2 → emsutil-0.4.0}/uv.lock +79 -1
  12. emsutil-0.2.2/src/emsutil/inexport/ffdata.py +0 -58
  13. {emsutil-0.2.2 → emsutil-0.4.0}/.gitignore +0 -0
  14. {emsutil-0.2.2 → emsutil-0.4.0}/.python-version +0 -0
  15. {emsutil-0.2.2 → emsutil-0.4.0}/README.md +0 -0
  16. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/const.py +0 -0
  17. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/inexport/strutil.py +0 -0
  18. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/isola.py +0 -0
  19. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/lib.py +0 -0
  20. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/plot/__init__.py +0 -0
  21. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/plot/plot2d.py +0 -0
  22. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/__init__.py +0 -0
  23. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/cmap_maker.py +0 -0
  24. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/display.py +0 -0
  25. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/display_settings.py +0 -0
  26. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/textures/background.png +0 -0
  27. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/textures/tex1.png +0 -0
  28. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/textures/tex2.png +0 -0
  29. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/textures/tex3.png +0 -0
  30. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/textures/tex4.png +0 -0
  31. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/textures/tex5.png +0 -0
  32. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/textures/tex6.png +0 -0
  33. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/pyvista/utils.py +0 -0
  34. {emsutil-0.2.2 → emsutil-0.4.0}/src/emsutil/rogers.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.2.2"
2
+ current_version = "0.4.0"
3
3
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
4
4
  serialize = ["{major}.{minor}.{patch}"]
5
5
  search = "{current_version}"
@@ -1,12 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emsutil
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: Common utilities for Emerge projects EMerge, Optycal and Heavi
5
5
  Project-URL: Homepage, https://github.com/FennisRobert/emsutil
6
6
  Project-URL: Issues, https://github.com/FennisRobert/emsutil/issues
7
7
  Requires-Python: >=3.10
8
8
  Requires-Dist: loguru>=0.7.3
9
9
  Requires-Dist: matplotlib>=3.8.0
10
+ Requires-Dist: msgpack-numpy>=0.4.8
11
+ Requires-Dist: msgpack>=1.1.2
10
12
  Requires-Dist: numpy<2.3,>=1.24
11
13
  Requires-Dist: pyvista>=0.46.4
12
14
  Requires-Dist: scipy>=1.14.0
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ # Resolve Python environment
5
+ PYTHON="$(command -v python)"
6
+
7
+ if [ -z "$PYTHON" ]; then
8
+ echo "ERROR: No active Python environment found."
9
+ exit 1
10
+ fi
11
+
12
+ echo "Using Python:"
13
+ echo " $PYTHON"
14
+
15
+ # Location of bundled installer
16
+ INSTALLER_ROOT="$HOME/.emerge/installer"
17
+
18
+ if [ ! -f "$INSTALLER_ROOT/install.sh" ]; then
19
+ echo "ERROR: MUMPS installer not found."
20
+ echo "Expected:"
21
+ echo " $INSTALLER_ROOT/install.sh"
22
+ exit 1
23
+ fi
24
+
25
+ # Run installer
26
+ bash "$INSTALLER_ROOT/install.sh"
27
+
28
+ # Install Python package into *this* environment
29
+ "$PYTHON" -m pip install "$HOME/.emerge/mumps_py"
30
+
31
+ echo
32
+ echo "MUMPS successfully installed for this Python environment."
@@ -4,13 +4,15 @@ allow-direct-references = true
4
4
 
5
5
  [project]
6
6
  name = "emsutil"
7
- version = "0.2.2"
7
+ version = "0.4.0"
8
8
  description = "Common utilities for Emerge projects EMerge, Optycal and Heavi"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "loguru>=0.7.3",
13
13
  "matplotlib>=3.8.0",
14
+ "msgpack>=1.1.2",
15
+ "msgpack-numpy>=0.4.8",
14
16
  "numpy>=1.24, <2.3",
15
17
  "pyvista>=0.46.4",
16
18
  "scipy>=1.14.0",
@@ -4,4 +4,5 @@ from .emdata import FarFieldComponent, EHFieldFF, EHField
4
4
  from .material import Material, MatProperty, FreqCoordDependent, FreqDependent, CoordDependent
5
5
  from .lib import EISO, EOMNI, EPS0, AIR, PEC, COPPER
6
6
  from .pyvista.display_settings import EMergeTheme
7
- from . import themes
7
+ from . import themes
8
+ from .file import Saveable, save_object, load_object
@@ -3,10 +3,13 @@ from dataclasses import dataclass, field
3
3
  from typing import Literal, Callable
4
4
  from .const import Z0, EPS0
5
5
  from .lib import EISO, EOMNI
6
+ from .file import Saveable
6
7
 
7
8
  @dataclass
8
- class FarFieldComponent:
9
+ class FarFieldComponent(Saveable):
9
10
  F: np.ndarray
11
+ _th: np.ndarray
12
+ _ph: np.ndarray
10
13
 
11
14
  @property
12
15
  def x(self) -> np.ndarray:
@@ -22,16 +25,16 @@ class FarFieldComponent:
22
25
 
23
26
  @property
24
27
  def theta(self) -> np.ndarray:
25
- thx = np.cos(self.theta)*np.cos(self.phi)
26
- thy = np.cos(self.theta)*np.sin(self.phi)
27
- thz = -np.sin(self.theta)
28
+ thx = np.cos(self._th)*np.cos(self._ph)
29
+ thy = np.cos(self._th)*np.sin(self._ph)
30
+ thz = -np.sin(self._th)
28
31
  return thx*self.F[0,:] + thy*self.F[1,:] + thz*self.F[2,:]
29
32
 
30
33
  @property
31
34
  def phi(self) -> np.ndarray:
32
- phx = -np.sin(self.phi)
33
- phy = np.cos(self.phi)
34
- phz = np.zeros_like(self.theta)
35
+ phx = -np.sin(self._ph)
36
+ phy = np.cos(self._ph)
37
+ phz = np.zeros_like(self._th)
35
38
  return phx*self.F[0,:] + phy*self.F[1,:] + phz*self.F[2,:]
36
39
 
37
40
  @property
@@ -55,7 +58,7 @@ class FarFieldComponent:
55
58
 
56
59
 
57
60
  @dataclass
58
- class FieldPlotData:
61
+ class FieldPlotData(Saveable):
59
62
  x: np.ndarray
60
63
  y: np.ndarray
61
64
  z: np.ndarray
@@ -80,6 +83,16 @@ class FieldPlotData:
80
83
  """
81
84
  return (self.x, self.y, self.z, self.F)
82
85
 
86
+ @property
87
+ def xyzvec(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
88
+ """Returns the X, Y, Z, F arrays.
89
+
90
+ Returns:
91
+ tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: The X, Y, Z, F arrays
92
+ """
93
+ return (self.x, self.y, self.z, self.vx, self.vy, self.vz)
94
+
95
+
83
96
  @property
84
97
  def xyzftri(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
85
98
  """Returns the X, Y, Z, F + triangulation arrays.
@@ -115,13 +128,14 @@ class FieldPlotData:
115
128
  yield self.F
116
129
 
117
130
  @dataclass
118
- class EHFieldFF:
131
+ class EHFieldFF(Saveable):
119
132
  _E: np.ndarray
120
133
  _H: np.ndarray
121
134
  theta: np.ndarray
122
135
  phi: np.ndarray
123
136
  Ptot: float
124
137
  ang: np.ndarray | None = field(default=None)
138
+ freq: float | None = field(default=None)
125
139
 
126
140
  def total_radiated_power_integral(
127
141
  self,
@@ -183,11 +197,11 @@ class EHFieldFF:
183
197
 
184
198
  @property
185
199
  def E(self) -> np.ndarray:
186
- return FarFieldComponent(self._E)
200
+ return FarFieldComponent(self._E, self.theta, self.phi)
187
201
 
188
202
  @property
189
203
  def H(self) -> np.ndarray:
190
- return FarFieldComponent(self._H)
204
+ return FarFieldComponent(self._H, self.theta, self.phi)
191
205
 
192
206
  @property
193
207
  def Ex(self) -> np.ndarray:
@@ -244,16 +258,16 @@ class EHFieldFF:
244
258
  @property
245
259
  def gain(self, kind: Literal['iso','omni'] = 'iso') -> FarFieldComponent:
246
260
  if kind=='iso':
247
- return FarFieldComponent(self._E/EISO)
261
+ return FarFieldComponent(self._E/EISO, self.theta, self.phi)
248
262
  else:
249
- return FarFieldComponent(self._E/EOMNI)
263
+ return FarFieldComponent(self._E/EOMNI, self.theta, self.phi)
250
264
 
251
265
  @property
252
266
  def dir(self, kind: Literal['iso','omni'] = 'iso') -> FarFieldComponent:
253
267
  if kind=='iso':
254
- return FarFieldComponent(self._E/(EISO*(self.Ptot)**0.5))
268
+ return FarFieldComponent(self._E/(EISO*(self.Ptot)**0.5), self.theta, self.phi)
255
269
  else:
256
- return FarFieldComponent(self._E/(EOMNI*(self.Ptot)**0.5))
270
+ return FarFieldComponent(self._E/(EOMNI*(self.Ptot)**0.5), self.theta, self.phi)
257
271
 
258
272
  @property
259
273
  def normE(self) -> np.ndarray:
@@ -306,7 +320,7 @@ class EHFieldFF:
306
320
  return FieldPlotData(x=xs, y=ys, z=zs, F=F)
307
321
 
308
322
  @dataclass
309
- class EHField:
323
+ class EHField(Saveable):
310
324
  _E: np.ndarray
311
325
  _H: np.ndarray
312
326
  x: np.ndarray
@@ -626,6 +640,7 @@ class EHField:
626
640
  """
627
641
  Fx, Fy, Fz = getattr(self, field)
628
642
 
643
+
629
644
  if metric=='real':
630
645
  Fx, Fy, Fz = Fx.real, Fy.real, Fz.real
631
646
  elif metric=='imag':
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ from scipy.sparse import csc_matrix, csr_matrix, isspmatrix
4
+ from numbers import Number
5
+ import msgpack
6
+ import msgpack_numpy as m
7
+
8
+ m.patch()
9
+
10
+
11
+ class Saveable:
12
+ save_fields: list[str] = None
13
+ skip_fields: list[str] = None
14
+ _registry = dict()
15
+
16
+ def __init_subclass__(cls, **kwargs):
17
+ super().__init_subclass__(**kwargs)
18
+ cls._registry[cls.__name__] = cls
19
+
20
+ @classmethod
21
+ def is_saveable(cls, obj) -> bool:
22
+ if not isinstance(obj, dict):
23
+ return False
24
+ if '__SAVEABLE__' in obj:
25
+ return True
26
+
27
+ def pack(self) -> dict:
28
+ if self.skip_fields is None:
29
+ skip = []
30
+ else:
31
+ skip = self.skip_fields
32
+
33
+ if self.save_fields is None:
34
+ data = dict()
35
+ data['__CLASS__'] = self.__class__.__name__
36
+ data['__SAVEABLE__'] = True
37
+ data['__FIELDS__'] = {key: value for key, value in self.__dict__.items()
38
+ if '__' not in key and key not in skip}
39
+ return data
40
+ else:
41
+ data = dict()
42
+ data['__CLASS__'] = self.__class__.__name__
43
+ data['__SAVEABLE__'] = True
44
+ data['__FIELDS__'] = {key: value for key, value in self.__dict__.items() if key in self.save_fields and key not in skip}
45
+ return data
46
+
47
+ @staticmethod
48
+ def unpack(data: dict, collection: dict) -> Saveable:
49
+ cls = Saveable._registry[data['__CLASS__']]
50
+ fields = data['__FIELDS__']
51
+ obj = cls.__new__(cls)
52
+ for field, value in fields.items():
53
+ setattr(obj, field, _unpack_data(value, collection))
54
+ return obj
55
+
56
+ def _is_set(data) -> bool:
57
+ if not isinstance(data, dict):
58
+ return False
59
+ if '__ISSET__' in data:
60
+ return True
61
+
62
+ def _pack_set(data) -> dict:
63
+ return {
64
+ '__ISSET__': True,
65
+ '__DATA__': list(data)
66
+ }
67
+
68
+ def _is_type(data) -> bool:
69
+ if not isinstance(data, dict):
70
+ return False
71
+ if '__CLASS_TYPE__' in data:
72
+ return True
73
+
74
+ def _pack_type(data) -> dict:
75
+ return {
76
+ '__CLASS_TYPE__': True,
77
+ '__CLASS_NAME__': f"{data.__module__}.{data.__qualname__}"
78
+ }
79
+
80
+ def _is_saved(data) -> bool:
81
+ if not isinstance(data, dict):
82
+ return False
83
+ if '__SAVED_OBJ__' in data:
84
+ return True
85
+
86
+ def _pack_saved(data) -> dict:
87
+ return {
88
+ '__SAVED_OBJ__': True,
89
+ '__OBJ_ID__': id(data)
90
+ }
91
+
92
+ def _unpack_saved(data: dict) -> int:
93
+ return data['__OBJ_ID__']
94
+
95
+ def _unpack_type(data: dict):
96
+ class_name = data['__CLASS_NAME__']
97
+ module_name, _, cls_name = class_name.rpartition('.')
98
+ module = __import__(module_name, fromlist=[cls_name])
99
+ cls = getattr(module, cls_name)
100
+ return cls
101
+
102
+ def _unpack_set(data: dict) -> set:
103
+ return set(data['__DATA__'])
104
+
105
+ def _pack_object(obj, _obj_collector: dict = None) -> dict:
106
+ if obj is None:
107
+ return obj
108
+ if isinstance(obj, (str, Number, np.ndarray, bool)):
109
+ return obj
110
+ elif isinstance(obj, set):
111
+ return _pack_set(obj)
112
+ elif isspmatrix(obj):
113
+ return obj
114
+ elif isinstance(obj, list):
115
+ return [_pack_object(item, _obj_collector) for item in obj]
116
+ elif isinstance(obj, tuple):
117
+ return tuple(_pack_object(item, _obj_collector) for item in obj)
118
+ elif isinstance(obj, dict):
119
+ return {key: _pack_object(value, _obj_collector) for key, value in obj.items()}
120
+ elif isinstance(obj, Saveable):
121
+ if id(obj) in _obj_collector:
122
+ return _obj_collector[id(obj)]['reference']
123
+ data = _pack_saved(obj)
124
+ _obj_collector[data['__OBJ_ID__']] = {'object': _pack_object(obj.pack(), _obj_collector), 'reference': data}
125
+ return data
126
+ elif isinstance(obj, type):
127
+ return _pack_type(obj)
128
+ else:
129
+ print(f'Trying to unpack {obj} of type {type(obj)}')
130
+ raise TypeError(f"Cannot pack object of type {type(obj)}")
131
+
132
+ def _unpack_data(data, _obj_collection):
133
+ if _is_saved(data):
134
+ obj_id = _unpack_saved(data)
135
+ saved_data = _obj_collection[obj_id]['object']
136
+ return _unpack_data(saved_data, _obj_collection)
137
+ if Saveable.is_saveable(data):
138
+ return Saveable.unpack(data, _obj_collection)
139
+ elif data is None:
140
+ return None
141
+ elif _is_type(data):
142
+ return _unpack_type(data)
143
+ elif _is_set(data):
144
+ return _unpack_set(data)
145
+ elif isinstance(data, np.ndarray):
146
+ return data.copy()
147
+ elif isinstance(data, (str, Number, np.ndarray, bool)):
148
+ return data
149
+ elif isinstance(data, list):
150
+ return [_unpack_data(item, _obj_collection) for item in data]
151
+ elif isinstance(data, tuple):
152
+ return tuple(_unpack_data(item, _obj_collection) for item in data)
153
+ elif isinstance(data, dict):
154
+ return {key: _unpack_data(value, _obj_collection) for key, value in data.items()}
155
+ elif isspmatrix(data):
156
+ return data
157
+ else:
158
+ raise TypeError(f"Cannot unpack data of type {type(data)}")
159
+
160
+ def save_object(filename: str, data: dict):
161
+ """Saves a class to a file using msgpack serialization.
162
+
163
+ Args:
164
+ filename (str): The filename for the final object
165
+ data (dict): The data to store
166
+ """
167
+ obj_collector = dict()
168
+ dataset = _pack_object(data, obj_collector)
169
+ save_dict = {
170
+ 'data': dataset,
171
+ 'objects': obj_collector
172
+ }
173
+
174
+ with open(filename, 'wb') as f:
175
+ f.write(msgpack.packb(save_dict, use_bin_type=True))
176
+
177
+ def load_object(filename: str) -> dict:
178
+ """Loads a class from a file using msgpack serialization.
179
+
180
+ Args:
181
+ filename (str): The filename of the dataset to load
182
+
183
+ Returns:
184
+ dict: The dict() object with the loaded data.
185
+ """
186
+ with open(filename, 'rb') as f:
187
+ loaded = msgpack.unpackb(f.read(), raw=False, strict_map_key=False, use_list=False)
188
+
189
+ data = loaded['data']
190
+ obj_collection = loaded['objects']
191
+ return _unpack_data(data, obj_collection)
192
+
@@ -0,0 +1,304 @@
1
+ import numpy as np
2
+ from .strutil import arry_to_line, matrix_to_lines, arry_to_fwl
3
+ from ..emdata import EHFieldFF
4
+ from dataclasses import dataclass
5
+ from typing import List
6
+ from loguru import logger
7
+ import os
8
+
9
+ @dataclass
10
+ class Column:
11
+ name: str
12
+ values: np.ndarray
13
+
14
+ @staticmethod
15
+ def from_array(name: str, data: np.ndarray) -> List["Column"]:
16
+ """
17
+ Returns:
18
+ - [Column] if data is real
19
+ - [Column(real), Column(imag)] if data is complex
20
+ """
21
+ data = np.asarray(data)
22
+
23
+ if np.iscomplexobj(data):
24
+ return [
25
+ Column(f"{name}[re]", data.real),
26
+ Column(f"{name}[im]", data.imag),
27
+ ]
28
+ else:
29
+ return [Column(name, data)]
30
+
31
+
32
+ def to_lines(
33
+ columns: List[Column],
34
+ header_prefix: str = "$ ",
35
+ separator: str = " ",
36
+ fmt: str = "{:g}",
37
+ ) -> List[str]:
38
+ """
39
+ Convert columns to equally spaced text lines.
40
+ """
41
+
42
+ # Convert all values to strings first
43
+ str_cols = [[fmt.format(v) for v in col.values] for col in columns]
44
+ headers = [col.name for col in columns]
45
+ headers[0] = header_prefix + headers[0]
46
+ for i in range(len(headers)-1):
47
+ headers[i] = headers[i] + ';'
48
+ # Compute column widths (including separator!)
49
+ widths = []
50
+ for h, col in zip(headers, str_cols):
51
+ widths.append(max(len(h), max(len(v) for v in col)))
52
+
53
+ def format_row(items, sep: str):
54
+ return sep.join(item.ljust(w) for item, w in zip(items, widths))
55
+
56
+ lines = []
57
+
58
+ # Header
59
+ lines.append(format_row(headers, separator))
60
+
61
+ # Data rows
62
+ nrows = len(columns[0].values)
63
+ for i in range(nrows):
64
+ row = [col[i] for col in str_cols]
65
+ lines.append(format_row(row, separator))
66
+
67
+ return lines
68
+
69
+ class _fieldGetter:
70
+
71
+ def __init__(self, fields: list[EHFieldFF]):
72
+ self.names: list[str] = []
73
+ self.data: list = fields
74
+
75
+ @property
76
+ def name(self) -> str:
77
+ return '.'.join([n.capitalize() for n in self.names])
78
+
79
+ def __getattr__(self, name: str) -> EHFieldFF:
80
+ self.names.append(name)
81
+ self.data = [getattr(item, name) for item in self.data]
82
+ return self
83
+
84
+ class FarFieldExporter:
85
+ """A class to export far-field data to a text file.
86
+
87
+ """
88
+ def __init__(self, filename: str, fields: list[EHFieldFF], precision: int = 4, todeg: bool = True):
89
+ self.filename: str = filename
90
+ self.fields: list[EHFieldFF] = fields
91
+ self.precision: int = precision
92
+ self.columns: list[_fieldGetter] = []
93
+ self.todeg: bool = todeg
94
+
95
+ def addcol(self) -> EHFieldFF:
96
+ """Returns a field getter to specify which field component to export.
97
+
98
+ Example:
99
+ >>> exporter = FarFieldExporter('data.txt', field_data)
100
+ >>> exporter.addcol().Ex
101
+ >>> exporter.addcol().gain.theta
102
+ >>> exporter.write()
103
+
104
+ Returns:
105
+ EHFieldFF(_fieldGetter): Field getter to specify field component.
106
+ """
107
+ getter = _fieldGetter(self.fields)
108
+ self.columns.append(getter)
109
+ return getter
110
+
111
+ def write(self) -> None:
112
+ """Saves the far-field data to a text file.
113
+ """
114
+ base = self.fields[0]
115
+
116
+ thetas = base.theta
117
+ phis = base.phi
118
+
119
+ theta_lin = thetas[0,:].flatten()
120
+ phi_lin = phis[:,0].flatten()
121
+ p = self.precision
122
+ mult = 1.0
123
+ unit = 'rad'
124
+ if self.todeg:
125
+ mult = 180.0 / np.pi
126
+ theta_lin = theta_lin * mult
127
+ phi_lin = phi_lin * mult
128
+ thetas = thetas * mult
129
+ phis = phis * mult
130
+ unit = 'deg'
131
+
132
+ datalines = []
133
+ datalines.append(f'% Theta({unit})')
134
+ datalines.append(' '.join([f'{x:.{p}g}' for x in theta_lin]))
135
+ datalines.append(f'% Phi({unit})')
136
+ datalines.append(' '.join([f'{x:.{p}g}' for x in phi_lin]))
137
+ datalines.append('% Frequencies(Hz)')
138
+ datalines.append(' '.join([f'{field.freq:.{p}g}' for field in self.fields]))
139
+ datalines.append('')
140
+ thcol = Column(f'Theta({unit})', thetas.flatten())
141
+ phcol = Column(f'Phi({unit})', phis.flatten())
142
+
143
+ for i, field in enumerate(self.fields):
144
+ freq = field.freq
145
+ cols = []
146
+ datalines.append(f'# {freq:.{p}g} Hz')
147
+ for col in self.columns:
148
+ cols.extend(Column.from_array(col.name, col.data[i].flatten()))
149
+
150
+ datalines.extend(to_lines([thcol, phcol] + cols, fmt=f"{{:.{p}g}}"))
151
+ datalines.append('')
152
+
153
+ #replace any extension of filename with.emff
154
+ filename = os.path.splitext(self.filename)[0] + '.emff'
155
+ self.filename = filename
156
+ with open(self.filename, 'w') as f:
157
+ f.write('\n'.join(datalines))
158
+ logger.info(f'Wrote far-field data to {self.filename}')
159
+
160
+ def import_farfield(filename: str, degrees: bool = True) -> dict[str, np.ndarray]:
161
+ """Reads emerge farfiel datafiles created by FarFieldExporter.
162
+ They can be recognized by the .emff extension.
163
+
164
+ Args:
165
+ filename (str): The path to the far-field data file.
166
+ degrees (bool): If True, theta and phi angles are converted to degrees.
167
+
168
+ Returns:
169
+ dict[str, np.ndarray]: The imported far-field data structured as:
170
+ {
171
+ "theta": np.ndarray, # Theta grid (nTheta, nPhi)
172
+ "phi": np.ndarray, # Phi grid (nTheta, nPhi)
173
+ "freq": np.ndarray, # Frequencies (nF,)
174
+ "Ex": np.ndarray, # Electric field component Ex (nF, nTheta, nPhi)
175
+ "Ey": np.ndarray, # Electric field component Ey (nF, nTheta, nPhi)
176
+ ...
177
+ """
178
+ with open(filename, "r") as f:
179
+ lines = f.readlines()
180
+
181
+ # ---------------- helpers ----------------
182
+ def parse_vector(header):
183
+ i = lines.index(header) + 1
184
+ return np.array(lines[i].split(), float)
185
+
186
+ def strip_unit(name):
187
+ if "(" in name and ")" in name:
188
+ base, unit = name.split("(")
189
+ return base.strip(), unit.rstrip(")")
190
+ return name.strip(), None
191
+
192
+ def to_rad(values, unit):
193
+ if unit == "deg":
194
+ return np.deg2rad(values)
195
+ return values
196
+
197
+ # ---------------- global grids ----------------
198
+ theta_name, theta_unit = strip_unit("% Theta(deg)")
199
+ phi_name, phi_unit = strip_unit("% Phi(deg)")
200
+
201
+ # Find actual headers (deg or rad)
202
+ theta_header = next(l for l in lines if l.startswith("% Theta"))
203
+ phi_header = next(l for l in lines if l.startswith("% Phi"))
204
+
205
+ theta_vals = parse_vector(theta_header)
206
+ phi_vals = parse_vector(phi_header)
207
+ freqs = parse_vector("% Frequencies(Hz)\n")
208
+
209
+ theta_unit = theta_header.split("(")[1].split(")")[0]
210
+ phi_unit = phi_header.split("(")[1].split(")")[0]
211
+
212
+ theta_vals = to_rad(theta_vals, theta_unit)
213
+ phi_vals = to_rad(phi_vals, phi_unit)
214
+
215
+ nTheta = len(theta_vals)
216
+ nPhi = len(phi_vals)
217
+ nF = len(freqs)
218
+
219
+ theta_grid, phi_grid = np.meshgrid(theta_vals, phi_vals, indexing="ij")
220
+
221
+ result = {
222
+ "theta": theta_grid,
223
+ "phi": phi_grid,
224
+ "freq": freqs,
225
+ }
226
+
227
+ # ---------------- frequency blocks ----------------
228
+ field_buffers = {}
229
+ column_map = {}
230
+ header_info = {}
231
+ fidx = -1
232
+ header_cols = None
233
+ data_rows = []
234
+
235
+ for line in lines:
236
+ line = line.strip()
237
+ if not line:
238
+ continue
239
+
240
+ # Frequency block
241
+ if line.startswith("#"):
242
+ fidx += 1
243
+ data_rows = []
244
+ continue
245
+
246
+ # Header line
247
+ if line.startswith("$"):
248
+ header_cols = []
249
+ column_map.clear()
250
+ header_info.clear()
251
+
252
+ raw_cols = line[1:].split(";")
253
+ for i, raw in enumerate(raw_cols):
254
+ name, unit = strip_unit(raw)
255
+ header_cols.append(name)
256
+ header_info[name] = unit
257
+
258
+ if "[" in name and "]" in name:
259
+ base, comp = name.split("[")
260
+ comp = comp.rstrip("]")
261
+ column_map.setdefault(base, {})[comp] = i
262
+ else:
263
+ column_map[name] = i
264
+ continue
265
+
266
+ if line.startswith("%"):
267
+ continue
268
+
269
+ parts = line.split()
270
+ if header_cols is None or len(parts) < len(header_cols):
271
+ continue
272
+
273
+ data_rows.append([float(p) for p in parts])
274
+
275
+ if len(data_rows) == nTheta * nPhi:
276
+ data = np.array(data_rows)
277
+
278
+ for field, comps in column_map.items():
279
+ unit = header_info.get(field)
280
+
281
+ if isinstance(comps, dict): # complex
282
+ re = data[:, comps["re"]]
283
+ im = data[:, comps["im"]]
284
+ arr = re + 1j * im
285
+ else: # real
286
+ arr = data[:, comps]
287
+
288
+ if unit in ("deg", "rad"):
289
+ arr = to_rad(arr, unit)
290
+
291
+ arr = arr.reshape(nTheta, nPhi)
292
+ field_buffers.setdefault(field, []).append(arr)
293
+
294
+
295
+ # Stack frequencies
296
+ for field, blocks in field_buffers.items():
297
+ result[field] = np.stack(blocks, axis=0)
298
+
299
+ if degrees:
300
+ result["theta"] = np.rad2deg(result["theta"])
301
+ result["phi"] = np.rad2deg(result["phi"])
302
+ result["Theta"] = np.rad2deg(result["Theta"])
303
+ result["Phi"] = np.rad2deg(result["Phi"])
304
+ return result
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
  import numpy as np
20
20
  from typing import Callable
21
21
  import inspect
22
+ from .file import Saveable
22
23
 
23
24
  C0 = 299792458
24
25
 
@@ -45,15 +46,16 @@ def _to_mat(value: float | complex | int | np.ndarray) -> np.ndarray:
45
46
  else:
46
47
  return ValueError(f'Trying to parse {value} as a material property tensor but it cant be identified as scalar, vector or matrix')
47
48
 
48
- class MatProperty:
49
+ class MatProperty(Saveable):
49
50
  _freq_dependent: bool = False
50
51
  _coord_dependent: bool = False
51
52
  _pickle_exclude = {"_func","_fmax"}
53
+ skip_fields = ("_func","_fmax")
52
54
  """The MatProperty class is an interface for EMerge to deal with frequency and coordinate dependent material properties
53
55
  """
54
56
 
55
57
  def __init__(self, value: float | complex | int | np.ndarray):
56
- self.value: np.ndarray = _to_mat(value)
58
+ self._value: np.ndarray = _to_mat(value)
57
59
 
58
60
  self._apply_to: np.ndarray = np.array([], dtype=np.int64)
59
61
  self._x: np.ndarray = np.array([], dtype=np.float64)
@@ -69,11 +71,15 @@ class MatProperty:
69
71
  self._z = np.concatenate([self._z, z])
70
72
 
71
73
  def __call__(self, f: float, data: np.ndarray) -> np.ndarray:
72
- data[:,:,self._apply_to] = np.repeat(self.value[:,:,np.newaxis], self._apply_to.shape[0], axis=2)
74
+ data[:,:,self._apply_to] = np.repeat(self._value[:,:,np.newaxis], self._apply_to.shape[0], axis=2)
73
75
  return data
74
76
 
77
+ @property
78
+ def value(self) -> float:
79
+ return self._value[0,0]
80
+
75
81
  def scalar(self, f: float):
76
- return self.value[0,0]
82
+ return self._value[0,0]
77
83
 
78
84
  def reset(self) -> None:
79
85
  self._apply_to: np.ndarray = np.array([], dtype=np.int64)
@@ -93,10 +99,10 @@ class MatProperty:
93
99
  for k in self._pickle_exclude:
94
100
  setattr(self, k, None)
95
101
 
96
- class FreqDependent(MatProperty):
102
+ class FreqDependent(MatProperty, Saveable):
97
103
  _freq_dependent: bool = True
98
104
  _coord_dependent: bool = False
99
-
105
+ skip_fields = ("_func","_fmax")
100
106
  def __init__(self,
101
107
  scalar: Callable | None = None,
102
108
  vector: Callable | None = None,
@@ -115,8 +121,6 @@ class FreqDependent(MatProperty):
115
121
  vector (Callable | None, optional): The diagonal rank-2 tensor function returning a (3,) array. Defaults to None.
116
122
  matrix (Callable | None, optional): The rank-2 tensor function returning a (3,3) array. Defaults to None.
117
123
 
118
- Returns:
119
- _type_: _description_
120
124
  """
121
125
  if scalar is not None:
122
126
  def _func(f: float) -> np.ndarray:
@@ -144,12 +148,17 @@ class FreqDependent(MatProperty):
144
148
  data[:,:,self._apply_to] = np.repeat(self._func(f)[:,:,np.newaxis], self._apply_to.shape[0], axis=2)
145
149
  return data
146
150
 
151
+ @property
152
+ def value(self) -> float:
153
+ raise ValueError('Frequency dependent material properties have no fixed value. Use the scalar(f) method to get the value at a specific frequency.')
154
+
147
155
  def scalar(self, f: float):
148
156
  return self._func(f)[0,0]
149
157
 
150
- class CoordDependent(MatProperty):
158
+ class CoordDependent(MatProperty,Saveable):
151
159
  _freq_dependent: bool = False
152
160
  _coord_dependent: bool = True
161
+ skip_fields = ("_func","_fmax")
153
162
  def __init__(self,
154
163
  max_value: float,
155
164
  scalar: Callable | None = None,
@@ -173,9 +182,7 @@ class CoordDependent(MatProperty):
173
182
  scalar (Callable | None, optional): The scalar value function returning a float/complex. Defaults to None.
174
183
  vector (Callable | None, optional): The diagonal rank-2 tensor function returning a (3,) array. Defaults to None.
175
184
  matrix (Callable | None, optional): The rank-2 tensor function returning a (3,3) array. Defaults to None.
176
-
177
- Returns:
178
- _type_: _description_
185
+
179
186
  """
180
187
 
181
188
  if scalar is not None:
@@ -211,13 +218,17 @@ class CoordDependent(MatProperty):
211
218
  data[:,:,self._apply_to] = self._func(self._x, self._y, self._z)
212
219
  return data
213
220
 
221
+ @property
222
+ def value(self) -> float:
223
+ return self._func(0,0,0)[0,0]
224
+
214
225
  def scalar(self, f: float):
215
226
  return self._func(0,0,0)[0,0]
216
227
 
217
- class FreqCoordDependent(MatProperty):
228
+ class FreqCoordDependent(MatProperty, Saveable):
218
229
  _freq_dependent: bool = True
219
230
  _coord_dependent: bool = True
220
-
231
+ skip_fields = ("_func","_fmax")
221
232
  def __init__(self,
222
233
  max_value: float,
223
234
  scalar: Callable | None = None,
@@ -240,8 +251,6 @@ class FreqCoordDependent(MatProperty):
240
251
  vector (Callable | None, optional): The diagonal rank-2 tensor function returning a (3,) array. Defaults to None.
241
252
  matrix (Callable | None, optional): The rank-2 tensor function returning a (3,3) array. Defaults to None.
242
253
 
243
- Returns:
244
- _type_: _description_
245
254
  """
246
255
  if scalar is not None:
247
256
  def _func(f, x, y, z) -> np.ndarray:
@@ -277,10 +286,14 @@ class FreqCoordDependent(MatProperty):
277
286
  data[:,:,self._apply_to] = self._func(f,self._x, self._y,self._z)
278
287
  return data
279
288
 
289
+ @property
290
+ def value(self) -> float:
291
+ raise ValueError('Frequency and coordinate dependent material properties have no fixed value. Use the scalar(f) method to get the value at a specific frequency and coordinate.')
292
+
280
293
  def scalar(self, f: float):
281
294
  return self._func(f, 0,0,0)[0,0]
282
295
 
283
- class Material:
296
+ class Material(Saveable):
284
297
  """The Material class generalizes a material in the EMerge FEM environment.
285
298
 
286
299
  If a scalar value is provided for the relative permittivity or the relative permeability
@@ -295,7 +308,7 @@ class Material:
295
308
 
296
309
  """
297
310
  _pickle_exclude = {"_neff"}
298
-
311
+ skip_fields = ("_neff",)
299
312
  def __init__(self,
300
313
  er: float | complex | np.ndarray | MatProperty = 1.0,
301
314
  ur: float | complex | np.ndarray | MatProperty = 1.0,
@@ -330,10 +343,12 @@ class Material:
330
343
  self._neff: Callable = lambda f: np.sqrt(self.ur._fmax(f)*self.er._fmax(f))
331
344
  else:
332
345
  self._neff: Callable = lambda f: _neff
333
- hex_str = self.color.lstrip('#')
334
- self._color_rgb = tuple(int(hex_str[i:i+2], 16)/255.0 for i in (0, 2, 4))
335
346
  self._metal: bool = _metal
336
347
 
348
+ @property
349
+ def _color_rgb(self) -> tuple[float,float,float]:
350
+ return tuple(int(self.color.lstrip('#')[i:i+2], 16)/255.0 for i in (0, 2, 4))
351
+
337
352
  def __getstate__(self):
338
353
  state = self.__dict__.copy()
339
354
  for k in self._pickle_exclude:
@@ -382,18 +397,14 @@ class Material:
382
397
  @property
383
398
  def frequency_dependent(self) -> bool:
384
399
  """If The material property are at all frequency dependent.
385
-
386
- Returns:
387
- bool: _description_
400
+
388
401
  """
389
402
  return self.er._freq_dependent or self.ur._freq_dependent or self.tand._freq_dependent or self.cond._freq_dependent
390
403
 
391
404
  @property
392
405
  def coordinate_dependent(self) -> bool:
393
406
  """If the material properties are at all coordinate dependent
394
-
395
- Returns:
396
- bool: _description_
407
+
397
408
  """
398
409
  return self.er._coord_dependent or self.ur._coord_dependent or self.tand._coord_dependent or self.cond._coord_dependent
399
410
 
@@ -184,8 +184,66 @@ class _Document(EMergeTheme):
184
184
  "#4DBEEEFF",
185
185
  ]
186
186
 
187
+ class Stylish(EMergeTheme):
188
+ """ A custom EMerge theme. """
189
+ def define(self):
190
+ self.backgroung_grad_1 = "#FFFFFF"
191
+ self.backgroung_grad_2 = "#FFFFFF"
192
+ self.grid_color = "#676767FF"
193
+ self.brightness = 1.0
194
+
195
+ self.label_color = "#FFFFFF"
196
+ self.text_color = "#000000FF"
197
+ self.render_metal = True
198
+ self.line_width = 3.0
199
+
200
+ self.geo_edge_width = 3.0
201
+ self.geo_edge_color = "#000000ff"
202
+
203
+
204
+ self.draw_xax = False
205
+ self.draw_yax = False
206
+ self.draw_zax = False
207
+ self.draw_xplane = False
208
+ self.draw_yplane = False
209
+ self.draw_zplane = False
210
+ self.draw_xgrid = False
211
+ self.draw_ygrid = False
212
+ self.draw_zgrid = False
213
+
214
+ self.axis_x_color = "#FF0000FF"
215
+ self.axis_y_color = "#00FF00FF"
216
+ self.axis_z_color = "#0000FFFF"
187
217
 
218
+ self.aa_active = True
219
+ self.aa_samples = 5
220
+ self.cmap_npts = 64
221
+
222
+ self.draw_pvgrid = False
223
+ self.render_shadows = True
224
+ # Basic clear academic scales
225
+ # Amplitude is Jet
226
+ # Wave is blue to transparent to red
227
+
228
+ self.colormaps = {
229
+ 'amplitude': (("#0000FF", "#00FFFF", "#00FF00", "#FFFF00", "#FF0000"),(0.0, 0.25, 0.5, 0.75, 1.0)),
230
+ 'wave': (("#FF0000", "#FFAAAA00","#0000FF00", "#0000FF"), (0.0, 0.49, 0.51, 1.0)),
231
+ }
232
+
233
+ self.render_style = 'surface'
234
+
235
+ # Clear high contrast matlab colors for paper
236
+ self.line_color_cycle = [
237
+ "#003E67FF",
238
+ "#7F2600FF",
239
+ "#5B09A7FF",
240
+ "#0C5D14FF",
241
+ "#A35100FF",
242
+ "#6C006AFF",
243
+ ]
244
+
188
245
  VaporWave = _VaporWave()
189
246
  Vintage = _Vintage()
190
247
  Tron = _Tron()
191
248
  Document = _Document()
249
+ Stylish = Stylish()
@@ -279,11 +279,13 @@ wheels = [
279
279
 
280
280
  [[package]]
281
281
  name = "emsutil"
282
- version = "0.2.2"
282
+ version = "0.4.0"
283
283
  source = { editable = "." }
284
284
  dependencies = [
285
285
  { name = "loguru" },
286
286
  { name = "matplotlib" },
287
+ { name = "msgpack" },
288
+ { name = "msgpack-numpy" },
287
289
  { name = "numpy" },
288
290
  { name = "pyvista" },
289
291
  { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -294,6 +296,8 @@ dependencies = [
294
296
  requires-dist = [
295
297
  { name = "loguru", specifier = ">=0.7.3" },
296
298
  { name = "matplotlib", specifier = ">=3.8.0" },
299
+ { name = "msgpack", specifier = ">=1.1.2" },
300
+ { name = "msgpack-numpy", specifier = ">=0.4.8" },
297
301
  { name = "numpy", specifier = ">=1.24,<2.3" },
298
302
  { name = "pyvista", specifier = ">=0.46.4" },
299
303
  { name = "scipy", specifier = ">=1.14.0" },
@@ -560,6 +564,80 @@ wheels = [
560
564
  { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" },
561
565
  ]
562
566
 
567
+ [[package]]
568
+ name = "msgpack"
569
+ version = "1.1.2"
570
+ source = { registry = "https://pypi.org/simple" }
571
+ sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
572
+ wheels = [
573
+ { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
574
+ { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
575
+ { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
576
+ { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
577
+ { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
578
+ { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
579
+ { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
580
+ { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
581
+ { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
582
+ { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
583
+ { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
584
+ { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
585
+ { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
586
+ { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
587
+ { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
588
+ { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
589
+ { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
590
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
591
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
592
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
593
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
594
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
595
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
596
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
597
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
598
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
599
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
600
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
601
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
602
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
603
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
604
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
605
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
606
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
607
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
608
+ { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
609
+ { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
610
+ { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
611
+ { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
612
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
613
+ { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
614
+ { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
615
+ { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
616
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
617
+ { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
618
+ { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
619
+ { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
620
+ { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
621
+ { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
622
+ { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
623
+ { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
624
+ { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
625
+ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
626
+ ]
627
+
628
+ [[package]]
629
+ name = "msgpack-numpy"
630
+ version = "0.4.8"
631
+ source = { registry = "https://pypi.org/simple" }
632
+ dependencies = [
633
+ { name = "msgpack" },
634
+ { name = "numpy" },
635
+ ]
636
+ sdist = { url = "https://files.pythonhosted.org/packages/08/94/61e8aee142733ebfdc400a05bdac6e1763c4514bba3b42743d223f388450/msgpack-numpy-0.4.8.tar.gz", hash = "sha256:c667d3180513422f9c7545be5eec5d296dcbb357e06f72ed39cc683797556e69", size = 10923, upload-time = "2022-06-09T03:43:08.739Z" }
637
+ wheels = [
638
+ { url = "https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl", hash = "sha256:773c19d4dfbae1b3c7b791083e2caf66983bb19b40901646f61d8731554ae3da", size = 6919, upload-time = "2022-06-09T03:43:06.82Z" },
639
+ ]
640
+
563
641
  [[package]]
564
642
  name = "numpy"
565
643
  version = "2.2.6"
@@ -1,58 +0,0 @@
1
- import numpy as np
2
- from .strutil import arry_to_line, matrix_to_lines, arry_to_fwl
3
- from ..emdata import EHFieldFF
4
-
5
- def export_ffdata(filename: str,
6
- thetas: np.ndarray,
7
- phis: np.ndarray,
8
- frequencies: np.ndarray,
9
- fields: list[EHFieldFF],
10
- precision: int = 4) -> None:
11
-
12
- lines = []
13
- lines.append('% Theta (deg)')
14
- lines.append(arry_to_line(thetas, precision=precision))
15
- lines.append('% Phi (deg)')
16
- lines.append(arry_to_line(phis, precision=precision))
17
- lines.append('')
18
- nF = frequencies.shape[0]
19
-
20
- actual_frequencies = []
21
- blocks = []
22
-
23
- T, P = np.meshgrid(thetas, phis, indexing='ij')
24
-
25
- thetal = T.flatten()
26
- phil = P.flatten()
27
-
28
- for iF in range(nF):
29
- freq = frequencies[iF]
30
- actual_frequencies.append(freq)
31
-
32
- farfield = fields[iF]._E
33
- Fx = farfield[0,:,:].squeeze().flatten()
34
- Fy = farfield[1,:,:].squeeze().flatten()
35
- Fz = farfield[2,:,:].squeeze().flatten()
36
-
37
- block_lines = []
38
-
39
- block_lines.append(f'# {freq} (Hz)')
40
- block_lines.append('$ Theta(deg); Phi(deg); E_x[re](V/m); E_x[im](V/m); E_y[re](V/m); E_y[im](V/m); E_z[re](V/m); E_z[im](V/m)')
41
- positions = (0, 14, 24, 38, 52, 66, 80, 94)
42
- for th, ph, ex, ey, ez in zip(thetal, phil, Fx, Fy, Fz):
43
- re_ex = np.real(ex)
44
- im_ex = np.imag(ex)
45
- re_ey = np.real(ey)
46
- im_ey = np.imag(ey)
47
- re_ez = np.real(ez)
48
- im_ez = np.imag(ez)
49
- line_values = np.array([th, ph, re_ex, im_ex, re_ey, im_ey, re_ez, im_ez])
50
- line = arry_to_fwl(line_values, positions, precision=precision)
51
- block_lines.append(line)
52
-
53
- lines.extend(block_lines)
54
- lines.extend('')
55
-
56
- text = '\n'.join(lines)
57
- with open(filename, 'w') as f:
58
- f.write(text)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes