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

@@ -273,13 +273,17 @@ class Assembler:
273
273
 
274
274
  logger.debug('Implementing PEC Boundary Conditions.')
275
275
  pec_ids: list[int] = []
276
+ pec_tris: list[int] = []
276
277
 
277
278
  # Conductivity above al imit, consider it all PEC
278
279
  ipec = 0
280
+
279
281
  for itet in range(field.n_tets):
280
282
  if cond[0,0,itet] > self.settings.mw_3d_peclim:
281
283
  ipec+=1
282
284
  pec_ids.extend(field.tet_to_field[:,itet])
285
+ for tri in field.mesh.tet_to_tri[:,itet]:
286
+ pec_tris.append(tri)
283
287
  if ipec>0:
284
288
  logger.trace(f'Extended PEC with {ipec} tets with a conductivity > {self.settings.mw_3d_peclim}.')
285
289
 
@@ -295,9 +299,12 @@ class Assembler:
295
299
  eids = field.edge_to_field[:, ii]
296
300
  pec_ids.extend(list(eids))
297
301
 
302
+
298
303
  for ii in tri_ids:
299
304
  tids = field.tri_to_field[:, ii]
300
305
  pec_ids.extend(list(tids))
306
+
307
+ pec_tris.extend(tri_ids)
301
308
 
302
309
 
303
310
  ############################################################
@@ -412,7 +419,8 @@ class Assembler:
412
419
 
413
420
  simjob.port_vectors = port_vectors
414
421
  simjob.solve_ids = solve_ids
415
-
422
+ simjob._pec_tris = pec_tris
423
+
416
424
  if has_periodic:
417
425
  simjob.P = Pmat
418
426
  simjob.Pd = Pmat.getH()
@@ -876,7 +876,7 @@ class Microwave3D:
876
876
  def _run_adaptive_mesh(self,
877
877
  iteration: int,
878
878
  frequency: float,
879
- automatic_modal_analysis: bool = True) -> MWData:
879
+ automatic_modal_analysis: bool = True) -> tuple[MWData, list[int]]:
880
880
  """Executes a frequency domain study
881
881
 
882
882
  The study is distributed over "n_workers" workers.
@@ -965,7 +965,7 @@ class Microwave3D:
965
965
  self.solveroutine.reset()
966
966
  ### Compute S-parameters and return
967
967
  self._post_process([job,], [mats,])
968
- return self.data
968
+ return self.data, job._pec_tris
969
969
 
970
970
  def eigenmode(self, search_frequency: float,
971
971
  nmodes: int = 6,
@@ -773,6 +773,7 @@ class RectangularWaveguide(PortBC):
773
773
  logger.info(' - Constructing coordinate system from normal port')
774
774
  self.cs = Axis(self.selection.normal).construct_cs()
775
775
  logger.debug(f' - Port CS: {self.cs}')
776
+
776
777
  def get_basis(self) -> np.ndarray:
777
778
  return self.cs._basis
778
779
 
@@ -667,13 +667,51 @@ class MWField:
667
667
  return sum([self.excitation[mode.port_number]*self._fields[mode.port_number] for mode in self.port_modes]) # type: ignore
668
668
 
669
669
  def set_field_vector(self) -> None:
670
- """Defines the default excitation coefficients for the current dataset"""
670
+ """Defines the default excitation coefficients for the current dataset as an excitation of only port 1."""
671
671
  self.excitation = {key: 0.0 for key in self._fields.keys()}
672
672
  self.excitation[self.port_modes[0].port_number] = 1.0 + 0j
673
673
 
674
- def excite_port(self, number: int) -> None:
674
+ def excite_port(self, number: int, excitation: complex = 1.0 + 0.0j) -> None:
675
+ """Excite a single port provided by a given port number
676
+
677
+ Args:
678
+ number (int): The port number to excite
679
+ coefficient (complex): The port excitation. Defaults to 1.0 + 0.0j
680
+ """
681
+ self.excitation = {key: 0.0 for key in self._fields.keys()}
682
+ self.excitation[self.port_modes[number-1].port_number] = excitation
683
+
684
+ def set_excitations(self, *excitations: complex) -> None:
685
+ """Set bulk port excitations by an ordered array of excitation coefficients.
686
+
687
+ Returns:
688
+ *complex: A sequence of complex numbers
689
+ """
675
690
  self.excitation = {key: 0.0 for key in self._fields.keys()}
676
- self.excitation[self.port_modes[number].port_number] = 1.0 + 0j
691
+ for iport, coeff in enumerate(excitations):
692
+ self.excitation[self.port_modes[iport].port_number] = coeff
693
+
694
+ def combine_ports(self, p1: int, p2: int) -> MWField:
695
+ """Combines ports p1 and p2 into a cifferential and common mode port respectively.
696
+
697
+ The p1 index becomes the differential mode port
698
+ The p2 index becomes the common mode port
699
+
700
+ Args:
701
+ p1 (int): The first port number
702
+ p2 (int): The second port number
703
+
704
+ Returns:
705
+ MWField: _description_
706
+ """
707
+
708
+ fp1 = self._fields[p1]
709
+ fp2 = self._fields[p2]
710
+
711
+ self._fields[p1] = (fp1-fp2)/np.sqrt(2)
712
+ self._fields[p2] = (fp1+fp2)/np.sqrt(2)
713
+
714
+ return self
677
715
 
678
716
  @property
679
717
  def EH(self) -> tuple[np.ndarray, np.ndarray]:
@@ -720,7 +758,9 @@ class MWField:
720
758
  xf = xs.flatten()
721
759
  yf = ys.flatten()
722
760
  zf = zs.flatten()
761
+ logger.debug(f'Interpolating {xf.shape[0]} field points')
723
762
  Ex, Ey, Ez = self.basis.interpolate(self._field, xf, yf, zf, usenan=usenan)
763
+ logger.debug('E Interpolation complete')
724
764
  self.Ex = Ex.reshape(shp)
725
765
  self.Ey = Ey.reshape(shp)
726
766
  self.Ez = Ez.reshape(shp)
@@ -728,6 +768,7 @@ class MWField:
728
768
 
729
769
  constants = 1/ (-1j*2*np.pi*self.freq*(self._dur*MU0) )
730
770
  Hx, Hy, Hz = self.basis.interpolate_curl(self._field, xf, yf, zf, constants, usenan=usenan)
771
+ logger.debug('H Interpolation complete')
731
772
  ids = self.basis.interpolate_index(xf, yf, zf)
732
773
 
733
774
  self.er = self._der[ids].reshape(shp)
@@ -743,14 +784,15 @@ class MWField:
743
784
 
744
785
  return field
745
786
 
746
- def _solution_quality(self) -> tuple[np.ndarray, np.ndarray]:
787
+ def _solution_quality(self, solve_ids: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
747
788
  from .adaptive_mesh import compute_error_estimate
748
789
 
749
- error_tet, max_elem_size = compute_error_estimate(self)
790
+ error_tet, max_elem_size = compute_error_estimate(self, solve_ids)
750
791
  return error_tet, max_elem_size
751
792
 
752
793
  def boundary(self,
753
794
  selection: FaceSelection) -> EHField:
795
+ """ Interpolate the field on the node coordinates of the surface."""
754
796
  nodes = self.mesh.nodes
755
797
  x = nodes[0,:]
756
798
  y = nodes[1,:]
@@ -1146,6 +1188,54 @@ class MWScalarNdim:
1146
1188
  def S(self, i1: int, i2: int) -> np.ndarray:
1147
1189
  return self.Sp[...,self._portmap[i1], self._portmap[i2]]
1148
1190
 
1191
+ def combine_ports(self, p1: int, p2: int) -> MWScalarNdim:
1192
+ """Combine ports p1 and p2 into a differential and common mode port respectively.
1193
+
1194
+ The p1 index becomes the differential mode port
1195
+ The p2 index becomes the common mode port
1196
+
1197
+ Args:
1198
+ p1 (int): The first port number
1199
+ p2 (int): The second port number
1200
+
1201
+ Returns:
1202
+ MWScalarNdim: _description_
1203
+ """
1204
+ if p1==p2:
1205
+ raise ValueError('p1 and p2 must be different port numbers')
1206
+
1207
+ F, N, _ = self.Sp.shape
1208
+ p1 = p1-1
1209
+ p2 = p2-1
1210
+
1211
+ if not (0 <= p1 < N and 0 <= p2 < N):
1212
+ raise IndexError(f'Ports {p1+1} or {p2+1} are out of range {N}')
1213
+
1214
+ Sout = self.Sp.copy()
1215
+ ii, jj = p1, p2
1216
+ idx = np.ones(N, dtype=np.bool)
1217
+ idx[[ii,jj]] = False
1218
+ others = np.nonzero(idx)[0]
1219
+ isqrt2 = 1.0 / np.sqrt(2.0)
1220
+
1221
+ Sout[:, others, ii] = (self.Sp[:, others, ii] - self.Sp[:, others, jj]) * isqrt2
1222
+ Sout[:, others, jj] = (self.Sp[:, others, ii] + self.Sp[:, others, jj]) * isqrt2
1223
+ Sout[:, ii, others] = (self.Sp[:, ii, others] - self.Sp[:, jj, others]) * isqrt2
1224
+ Sout[:, jj, others] = (self.Sp[:, ii, others] + self.Sp[:, jj, others]) * isqrt2
1225
+
1226
+ Sii = self.Sp[:, ii, ii]
1227
+ Sij = self.Sp[:, ii, jj]
1228
+ Sji = self.Sp[:, jj, ii]
1229
+ Sjj = self.Sp[:, jj, jj]
1230
+
1231
+ Sout[:, ii, ii] = 0.5 *(Sii - Sij - Sji + Sjj)
1232
+ Sout[:, ii, jj] = 0.5 *(Sii + Sij - Sji - Sjj)
1233
+ Sout[:, jj, ii] = 0.5 *(Sii - Sij + Sji - Sjj)
1234
+ Sout[:, jj, jj] = 0.5 *(Sii + Sij + Sji + Sjj)
1235
+
1236
+ self.Sp = Sout
1237
+
1238
+ return self
1149
1239
  @property
1150
1240
  def Smat(self) -> np.ndarray:
1151
1241
  """Returns the full S-matrix
@@ -14,6 +14,7 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program; if not, see
16
16
  # <https://www.gnu.org/licenses/>.
17
+
17
18
  from __future__ import annotations
18
19
  from ...mesh3d import Mesh3D
19
20
  from ...simstate import SimState
@@ -43,21 +44,43 @@ cmap_names = Literal['bgy','bgyw','kbc','blues','bmw','bmy','kgy','gray','dimgra
43
44
  'bkr','bky','coolwarm','gwv','bjy','bwy','cwr','colorwheel','isolum','rainbow','fire',
44
45
  'cet_fire','gouldian','kbgyw','cwr','CET_CBL1','CET_CBL3','CET_D1A']
45
46
 
46
- EMERGE_AMP = make_colormap(["#1F0061","#35188e","#1531ab", "#ff007b", "#ff7c51"], (0.0, 0.2, 0.4, 0.7, 0.9))
47
+ EMERGE_AMP = make_colormap(["#1F0061","#4218c0","#2849db", "#ff007b", "#ff7c51"], (0.0, 0.15, 0.3, 0.7, 0.9))
47
48
  EMERGE_WAVE = make_colormap(["#4ab9ff","#0510B2B8","#3A37466E","#CC0954B9","#ff9036"], (0.0, 0.3, 0.5, 0.7, 1.0))
48
49
 
49
- def _gen_c_cycle():
50
- colors = [
50
+
51
+ ## Cycler class
52
+
53
+ class _Cycler:
54
+ """Like itertools.cycle(iterable) but with reset(). Materializes the iterable."""
55
+ def __init__(self, iterable):
56
+ self._data = list(iterable)
57
+ self._n = len(self._data)
58
+ self._i = 0
59
+
60
+ def __iter__(self):
61
+ return self
62
+
63
+ def __next__(self):
64
+ if self._n == 0:
65
+ raise StopIteration
66
+ item = self._data[self._i]
67
+ self._i += 1
68
+ if self._i == self._n:
69
+ self._i = 0
70
+ return item
71
+
72
+ def reset(self):
73
+ self._i = 0
74
+
75
+
76
+ C_CYCLE = _Cycler([
51
77
  "#0000aa",
52
78
  "#aa0000",
53
79
  "#009900",
54
80
  "#990099",
55
81
  "#994400",
56
82
  "#005588"
57
- ]
58
- return cycle(colors)
59
-
60
- C_CYCLE = _gen_c_cycle()
83
+ ])
61
84
 
62
85
  class _RunState:
63
86
 
@@ -242,6 +265,7 @@ class PVDisplay(BaseDisplay):
242
265
  self._stop: bool = False
243
266
  self._objs: list[_AnimObject] = []
244
267
  self._do_animate: bool = False
268
+ self._animate_next: bool = False
245
269
  self._closed_via_x: bool = False
246
270
  self._Nsteps: int = 0
247
271
  self._fps: int = 25
@@ -334,7 +358,9 @@ class PVDisplay(BaseDisplay):
334
358
  self._plot = pv.Plotter()
335
359
  self._stop = False
336
360
  self._objs = []
337
- C_CYCLE = _gen_c_cycle()
361
+ self._animate_next = False
362
+ self._reset_cbar()
363
+ C_CYCLE.reset()
338
364
 
339
365
  def _close_callback(self, arg):
340
366
  """The private callback function that stops the animation.
@@ -403,6 +429,7 @@ class PVDisplay(BaseDisplay):
403
429
  print('If you closed the animation without using (Q) press Ctrl+C to kill the process.')
404
430
  self._Nsteps = Nsteps
405
431
  self._fps = fps
432
+ self._animate_next = True
406
433
  self._do_animate = True
407
434
  return self
408
435
 
@@ -518,7 +545,7 @@ class PVDisplay(BaseDisplay):
518
545
 
519
546
  self._plot.add_mesh(self._volume_edges(_select(obj)), color='#000000', line_width=2, show_edges=True)
520
547
 
521
- if isinstance(obj, GeoObject) and label:
548
+ if label:
522
549
  points = []
523
550
  labels = []
524
551
  for dt in obj.dimtags:
@@ -691,14 +718,14 @@ class PVDisplay(BaseDisplay):
691
718
  actor = self._plot.add_mesh(grid_no_nan, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
692
719
 
693
720
 
694
- if self._do_animate:
721
+ if self._animate_next:
695
722
  def on_update(obj: _AnimObject, phi: complex):
696
723
  field_anim = obj.T(np.real(obj.field * phi))
697
724
  obj.grid[name] = field_anim
698
725
  obj.fgrid[name] = obj.grid.threshold(scalars=name)[name]
699
726
  #obj.fgrid replace with thresholded scalar data.
700
727
  self._objs.append(_AnimObject(field_flat, T, grid, grid_no_nan, actor, on_update))
701
-
728
+ self._animate_next = False
702
729
  self._reset_cbar()
703
730
 
704
731
  def add_boundary_field(self,
@@ -773,13 +800,13 @@ class PVDisplay(BaseDisplay):
773
800
  kwargs = setdefault(kwargs, cmap=cmap, clim=clim, opacity=opacity, pickable=False, multi_colors=True)
774
801
  actor = self._plot.add_mesh(grid, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
775
802
 
776
- if self._do_animate:
803
+ if self._animate_next:
777
804
  def on_update(obj: _AnimObject, phi: complex):
778
805
  field_anim = obj.T(np.real(obj.field * phi))
779
806
  obj.grid[name] = field_anim
780
807
  #obj.fgrid replace with thresholded scalar data.
781
808
  self._objs.append(_AnimObject(field_flat, T, grid, grid, actor, on_update))
782
-
809
+ self._animate_next = False
783
810
  self._reset_cbar()
784
811
 
785
812
  def add_title(self, title: str) -> None:
@@ -921,7 +948,7 @@ class PVDisplay(BaseDisplay):
921
948
 
922
949
  actor = self._plot.add_mesh(contour, opacity=opacity, cmap=cmap, clim=clim, pickable=False, scalar_bar_args=self._cbar_args)
923
950
 
924
- if self._do_animate:
951
+ if self._animate_next:
925
952
  def on_update(obj: _AnimObject, phi: complex):
926
953
  new_vals = obj.T(np.real(obj.field * phi))
927
954
  obj.grid['anim'] = new_vals
@@ -929,7 +956,7 @@ class PVDisplay(BaseDisplay):
929
956
  obj.actor.GetMapper().SetInputData(new_contour) # type: ignore
930
957
 
931
958
  self._objs.append(_AnimObject(field, T, grid, None, actor, on_update)) # type: ignore
932
-
959
+ self._animate_next = False
933
960
  self._reset_cbar()
934
961
 
935
962
  def _add_aux_items(self) -> None:
@@ -495,11 +495,11 @@ def plot_sp(f: np.ndarray | list[np.ndarray], S: list[np.ndarray] | np.ndarray,
495
495
  if isinstance(levelindicator, (int, float)) and levelindicator is not None:
496
496
  lvl = levelindicator
497
497
  fcross = hintersections(f, SdB, lvl)
498
- for fs in fcross:
498
+ for freqs in fcross:
499
499
  ax_mag.annotate(
500
- f"{str(fs)[:4]}{xunit}",
501
- xy=(fs, lvl),
502
- xytext=(fs + 0.08 * (max(f) - min(f)) / unitdivider[xunit], lvl),
500
+ f"{str(freqs)[:4]}{xunit}",
501
+ xy=(freqs, lvl),
502
+ xytext=(freqs + 0.08 * (max(f) - min(f)) / unitdivider[xunit], lvl),
503
503
  arrowprops=dict(facecolor="black", width=1, headwidth=5),
504
504
  )
505
505
  if fill_areas is not None:
@@ -541,6 +541,118 @@ def plot_sp(f: np.ndarray | list[np.ndarray], S: list[np.ndarray] | np.ndarray,
541
541
 
542
542
  return fig, ax_mag, ax_phase
543
543
 
544
+ def plot_vswr(f: np.ndarray | list[np.ndarray], S: list[np.ndarray] | np.ndarray,
545
+ swrlim=[1, 5],
546
+ xunit="GHz",
547
+ levelindicator: int | float | None = None,
548
+ fill_areas: list[tuple] | None = None,
549
+ spec_area: list[tuple[float,...]] | None = None,
550
+ labels: list[str] | None = None,
551
+ linestyles: list[str] | None = None,
552
+ colorcycle: list[int] | None = None,
553
+ filename: str | None = None,
554
+ show_plot: bool = True,
555
+ figdata: tuple | None = None) -> tuple[plt.Figure, plt.Axes, plt.Axes]:
556
+ """Plot S-parameters in VSWR
557
+
558
+ One may provide:
559
+ - A single frequency with a single S-parameter
560
+ - A single frequency with a list of S-parameters
561
+ - A list of frequencies with a list of S-parameters
562
+
563
+ Args:
564
+ f (np.ndarray | list[np.ndarray]): Frequency vector or list of frequencies
565
+ S (list[np.ndarray] | np.ndarray): S-parameters to plot (list or single array)
566
+ swrlim (list, optional): VSWR y-axis limit. Defaults to [1, 5].
567
+ xunit (str, optional): Frequency unit. Defaults to "GHz".
568
+ levelindicator (int | float, optional): Level at which annotation arrows will be added. Defaults to None.
569
+ fill_areas (list[tuple], optional): Regions to fill (fmin, fmax). Defaults to None.
570
+ spec_area (list[tuple[float]], optional): _description_. Defaults to None.
571
+ labels (list[str], optional): A lists of labels to use. Defaults to None.
572
+ linestyles (list[str], optional): The linestyle to use (list or single string). Defaults to None.
573
+ colorcycle (list[int], optional): A list of colors to use. Defaults to None.
574
+ filename (str, optional): The filename (will automatically save). Defaults to None.
575
+ show_plot (bool, optional): If or not to show the resulting plot. Defaults to True.
576
+
577
+ """
578
+ if not isinstance(S, list):
579
+ Ss = [S]
580
+ else:
581
+ Ss = S
582
+
583
+ if not isinstance(f, list):
584
+ fs = [f for _ in Ss]
585
+ else:
586
+ fs = f
587
+
588
+ if linestyles is None:
589
+ linestyles = ['-' for _ in S]
590
+
591
+ if colorcycle is None:
592
+ colorcycle = [i for i, S in enumerate(S)]
593
+
594
+ unitdivider: dict[str, float] = {"MHz": 1e6, "GHz": 1e9, "kHz": 1e3}
595
+
596
+ fs = [f / unitdivider[xunit] for f in fs]
597
+
598
+ if figdata is None:
599
+ # Create two subplots: one for magnitude and one for phase
600
+ fig, ax_swr = plt.subplots()
601
+ fig.subplots_adjust(hspace=0.3)
602
+ else:
603
+ fig, ax_swr = figdata
604
+ maxy = 5
605
+
606
+
607
+ for f, s, ls, cid in zip(fs, Ss, linestyles, colorcycle):
608
+ # Calculate and plot magnitude in dB
609
+ SWR = np.divide((1 + abs(s)), (1 - abs(s)))
610
+ ax_swr.plot(f, SWR, label="VSWR", linestyle=ls, color=EMERGE_COLORS[cid % len(EMERGE_COLORS)])
611
+ if np.max(SWR) > maxy:
612
+ maxy = np.max(SWR)
613
+
614
+ # Annotate level indicators if specified
615
+ if isinstance(levelindicator, (int, float)) and levelindicator is not None:
616
+ lvl = levelindicator
617
+ fcross = hintersections(f, SWR, lvl)
618
+ for fa in fcross:
619
+ ax_swr.annotate(
620
+ f"{str(fa)[:4]}{xunit}",
621
+ xy=(fa, lvl),
622
+ xytext=(fa + 0.08 * (max(f) - min(f)) / unitdivider[xunit], lvl),
623
+ arrowprops=dict(facecolor="black", width=1, headwidth=5),
624
+ )
625
+
626
+
627
+ if fill_areas is not None:
628
+ for fmin, fmax in fill_areas:
629
+ f1 = fmin / unitdivider[xunit]
630
+ f2 = fmax / unitdivider[xunit]
631
+ ax_swr.fill_between([f1, f2], swrlim[0], swrlim[1], color='grey', alpha= 0.2)
632
+
633
+ if spec_area is not None:
634
+ for fmin, fmax, vmin, vmax in spec_area:
635
+ f1 = fmin / unitdivider[xunit]
636
+ f2 = fmax / unitdivider[xunit]
637
+ ax_swr.fill_between([f1, f2], vmin,vmax, color='red', alpha=0.2)
638
+
639
+ # Configure magnitude plot (ax_swr)
640
+ fmin = min([min(f) for f in fs])
641
+ fmax = max([max(f) for f in fs])
642
+ ax_swr.set_ylabel("VSWR")
643
+ ax_swr.set_xlabel(f"Frequency ({xunit})")
644
+ ax_swr.axis([fmin, fmax, swrlim[0], max(maxy*1.1,swrlim[1])]) # type: ignore
645
+ ax_swr.xaxis.set_minor_locator(tck.AutoMinorLocator(2))
646
+ ax_swr.yaxis.set_minor_locator(tck.AutoMinorLocator(2))
647
+
648
+ if labels is not None:
649
+ ax_swr.legend(labels)
650
+ if show_plot:
651
+ plt.show()
652
+ if filename is not None:
653
+ fig.savefig(filename)
654
+
655
+ return fig, ax_swr
544
656
 
545
657
  def plot_ff(
546
658
  theta: np.ndarray | list[np.ndarray],
@@ -21,6 +21,9 @@ import numpy as np
21
21
  from .cs import Axis, CoordinateSystem, _parse_vector, Plane
22
22
  from typing import Callable, TypeVar, Iterable, Any
23
23
 
24
+ class SelectionError(Exception):
25
+ pass
26
+
24
27
  def align_rectangle_frame(pts3d: np.ndarray, normal: np.ndarray) -> dict[str, Any]:
25
28
  """Tries to find a rectangle as convex-hull of a set of points with a given normal vector.
26
29
 
@@ -174,7 +177,7 @@ class Selection:
174
177
  """
175
178
  dim: int = -1
176
179
  def __init__(self, tags: list[int] | set[int] | tuple[int] | None = None):
177
-
180
+ self.name: str = 'Selection'
178
181
  self._tags: set[int] = set()
179
182
 
180
183
  if tags is not None:
@@ -252,7 +255,19 @@ class Selection:
252
255
  maxy = max(maxy, y1)
253
256
  maxz = max(maxz, z1)
254
257
  return (minx, miny, minz), (maxx, maxy, maxz)
255
-
258
+
259
+ def _named(self, name: str) -> Selection:
260
+ """Sets the name of the selection and returns it
261
+
262
+ Args:
263
+ name (str): The name of the selection
264
+
265
+ Returns:
266
+ Selection: The same selection object
267
+ """
268
+ self.name = name
269
+ return self
270
+
256
271
  def exclude(self, xyz_excl_function: Callable = lambda x,y,z: True, plane: Plane | None = None, axis: Axis | None = None) -> Selection:
257
272
  """Exclude points by evaluating a function(x,y,z)-> bool
258
273