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

Files changed (33) hide show
  1. emerge/__init__.py +15 -3
  2. emerge/_emerge/const.py +2 -1
  3. emerge/_emerge/elements/ned2_interp.py +122 -42
  4. emerge/_emerge/geo/__init__.py +1 -1
  5. emerge/_emerge/geo/operations.py +20 -0
  6. emerge/_emerge/geo/pcb.py +162 -71
  7. emerge/_emerge/geo/shapes.py +12 -7
  8. emerge/_emerge/geo/step.py +177 -41
  9. emerge/_emerge/geometry.py +189 -27
  10. emerge/_emerge/logsettings.py +26 -2
  11. emerge/_emerge/material.py +2 -0
  12. emerge/_emerge/mesh3d.py +6 -8
  13. emerge/_emerge/mesher.py +67 -11
  14. emerge/_emerge/mth/common_functions.py +1 -1
  15. emerge/_emerge/mth/optimized.py +2 -2
  16. emerge/_emerge/physics/microwave/adaptive_mesh.py +549 -116
  17. emerge/_emerge/physics/microwave/assembly/assembler.py +9 -1
  18. emerge/_emerge/physics/microwave/microwave_3d.py +133 -83
  19. emerge/_emerge/physics/microwave/microwave_bc.py +158 -8
  20. emerge/_emerge/physics/microwave/microwave_data.py +94 -5
  21. emerge/_emerge/plot/pyvista/display.py +36 -23
  22. emerge/_emerge/selection.py +17 -2
  23. emerge/_emerge/settings.py +124 -6
  24. emerge/_emerge/simmodel.py +273 -150
  25. emerge/_emerge/simstate.py +106 -0
  26. emerge/_emerge/simulation_data.py +11 -23
  27. emerge/_emerge/solve_interfaces/cudss_interface.py +20 -1
  28. emerge/_emerge/solver.py +4 -4
  29. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/METADATA +7 -3
  30. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/RECORD +33 -32
  31. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/WHEEL +0 -0
  32. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/entry_points.txt +0 -0
  33. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -29,6 +29,7 @@ from ...bc import BoundaryCondition, BoundaryConditionSet, Periodic
29
29
  from ...periodic import PeriodicCell, HexCell, RectCell
30
30
  from ...material import Material
31
31
  from ...const import Z0, C0, EPS0, MU0
32
+ from ...logsettings import DEBUG_COLLECTOR
32
33
 
33
34
  ############################################################
34
35
  # UTILITY FUNCTIONS #
@@ -59,6 +60,7 @@ class MWBoundaryConditionSet(BoundaryConditionSet):
59
60
  self.RectangularWaveguide: type[RectangularWaveguide] = self._construct_bc(RectangularWaveguide)
60
61
  self.Periodic: type[Periodic] = self._construct_bc(Periodic)
61
62
  self.FloquetPort: type[FloquetPort] = self._construct_bc(FloquetPort)
63
+ self.UserDefinedPort: type[UserDefinedPort] = self._construct_bc(UserDefinedPort)
62
64
 
63
65
  self._cell: PeriodicCell | None = None
64
66
 
@@ -341,7 +343,6 @@ class PortMode:
341
343
  norm_factor: float = 1
342
344
  freq: float = 0
343
345
  neff: float = 1
344
- TEM: bool = True
345
346
  Z0: float = 50.0
346
347
  polarity: float = 1.0
347
348
  modetype: Literal['TEM','TE','TM'] = 'TEM'
@@ -459,7 +460,7 @@ class ModalPort(PortBC):
459
460
  port_number: int,
460
461
  cs: CoordinateSystem | None = None,
461
462
  power: float = 1,
462
- TEM: bool = False,
463
+ modetype: Literal['TE','TM','TEM'] | None = None,
463
464
  mixed_materials: bool = False):
464
465
  """Generes a ModalPort boundary condition for a port that requires eigenmode solutions for the mode.
465
466
 
@@ -476,7 +477,7 @@ class ModalPort(PortBC):
476
477
  port_number (int): The port number as an integer
477
478
  cs (CoordinateSystem, optional): The local coordinate system of the port face. Defaults to None.
478
479
  power (float, optional): The radiated power. Defaults to 1.
479
- TEM (bool, optional): Wether the mode should be considered as a TEM mode. Defaults to False
480
+ modetype (str[TE, TM, TEM], optional): Wether the mode should be considered as a TEM mode. Defaults to False
480
481
  mixed_materials (bool, optional): Wether the port consists of multiple different dielectrics. This requires
481
482
  A recalculation of the port mode at every frequency
482
483
  """
@@ -490,7 +491,7 @@ class ModalPort(PortBC):
490
491
  self.selected_mode: int = 0
491
492
  self.modes: dict[float, list[PortMode]] = defaultdict(list)
492
493
 
493
- self.TEM: bool = TEM
494
+ self.forced_modetype: Literal['TE','TM','TEM'] | None = modetype
494
495
  self.mixed_materials: bool = mixed_materials
495
496
  self.initialized: bool = False
496
497
  self._first_k0: float | None = None
@@ -506,7 +507,25 @@ class ModalPort(PortBC):
506
507
  raise ValueError('No Coordinate System could be derived.')
507
508
  self._er: np.ndarray | None = None
508
509
  self._ur: np.ndarray | None = None
510
+
511
+ self.vintline: list[Line] = []
512
+
513
+ def set_integration_line(self, c1: tuple[float, float, float], c2: tuple[float, float, float], N: int = 21) -> None:
514
+ """Define the integration line start and end point
515
+
516
+ Args:
517
+ c1 (tuple[float, float, float]): The start coordinate
518
+ c2 (tuple[float, float, float]): The end coordinate
519
+ N (int, optional): The number of integration points. Defaults to 21.
520
+ """
521
+ self.vintline.append(Line.from_points(c1, c2, N))
509
522
 
523
+ def reset(self) -> None:
524
+ self.modes: dict[float, list[PortMode]] = defaultdict(list)
525
+ self.initialized: bool = False
526
+ self.plus_terminal: list[tuple[int, int]] = []
527
+ self.minus_terminal: list[tuple[int, int]] = []
528
+
510
529
  def portZ0(self, k0: float) -> complex | float | None:
511
530
  return self.get_mode(k0).Z0
512
531
 
@@ -524,6 +543,11 @@ class ModalPort(PortBC):
524
543
  """
525
544
  self.alignment_vectors = [_parse_axis(ax) for ax in axes]
526
545
 
546
+ def _get_alignment_vector(self, index: int) -> np.ndarray | None:
547
+ if len(self.alignment_vectors) > index:
548
+ return self.alignment_vectors[index].np
549
+ return None
550
+
527
551
  def set_terminals(self, positive: Selection | GeoObject | None = None,
528
552
  negative: Selection | GeoObject | None = None,
529
553
  ground: Selection | GeoObject | None = None) -> None:
@@ -616,7 +640,7 @@ class ModalPort(PortBC):
616
640
  beta: float,
617
641
  k0: float,
618
642
  residual: float,
619
- TEM: bool,
643
+ number: int,
620
644
  freq: float) -> PortMode | None:
621
645
  """Add a mode function to the ModalPort
622
646
 
@@ -627,16 +651,17 @@ class ModalPort(PortBC):
627
651
  beta (float): The out-of-plane propagation constant
628
652
  k0 (float): The free space phase constant
629
653
  residual (float): The solution residual
630
- TEM (bool): Whether its a TEM mode
631
654
  freq (float): The frequency of the port mode
632
655
 
633
656
  Returns:
634
657
  PortMode: The port mode object.
635
658
  """
636
- mode = PortMode(field, E_function, H_function, k0, beta, residual, TEM=TEM, freq=freq)
659
+ mode = PortMode(field, E_function, H_function, k0, beta, residual, freq=freq)
660
+
637
661
  if mode.energy < 1e-4:
638
662
  logger.debug(f'Ignoring mode due to a low mode energy: {mode.energy}')
639
663
  return None
664
+
640
665
  self.modes[k0].append(mode)
641
666
  self.initialized = True
642
667
 
@@ -659,7 +684,7 @@ class ModalPort(PortBC):
659
684
 
660
685
  def get_beta(self, k0: float) -> float:
661
686
  mode = self.get_mode(k0)
662
- if mode.TEM:
687
+ if self.forced_modetype=='TEM':
663
688
  beta = mode.beta/mode.k0 * k0
664
689
  else:
665
690
  freq = k0*299792458/(2*np.pi)
@@ -748,6 +773,7 @@ class RectangularWaveguide(PortBC):
748
773
  logger.info(' - Constructing coordinate system from normal port')
749
774
  self.cs = Axis(self.selection.normal).construct_cs()
750
775
  logger.debug(f' - Port CS: {self.cs}')
776
+
751
777
  def get_basis(self) -> np.ndarray:
752
778
  return self.cs._basis
753
779
 
@@ -816,6 +842,125 @@ class RectangularWaveguide(PortBC):
816
842
  Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
817
843
  return np.array([Exg, Eyg, Ezg])
818
844
 
845
+ def _f_zero(k0,x,y,z):
846
+ "Zero field function"
847
+ return np.zeros_like(x, dtype=np.complex128)
848
+
849
+ class UserDefinedPort(PortBC):
850
+
851
+ _include_stiff: bool = True
852
+ _include_mass: bool = False
853
+ _include_force: bool = True
854
+
855
+ def __init__(self,
856
+ face: FaceSelection | GeoSurface,
857
+ port_number: int,
858
+ Ex: Callable | None = None,
859
+ Ey: Callable | None = None,
860
+ Ez: Callable | None = None,
861
+ kz: Callable | None = None,
862
+ power: float = 1.0,
863
+ modetype: Literal['TEM','TE','TM'] = 'TEM',
864
+ cs: CoordinateSystem | None = None):
865
+ """Creates a user defined port field
866
+
867
+ The UserDefinedPort is defined based on user defined field callables. All undefined callables will default to 0 field or k0.
868
+
869
+ All spatial field functions should be defined using the template:
870
+ >>> def Ec(k0: float, x: np.ndarray, y: np.ndarray, z: np.ndarray) -> np.ndarray
871
+ >>> return #shape like x
872
+
873
+ Args:
874
+ face (FaceSelection, GeoSurface): The port boundary face selection
875
+ port_number (int): The port number
876
+ Ex (Callable): The Ex(k0,x,y,z) field
877
+ Ey (Callable): The Ey(k0,x,y,z) field
878
+ Ez (Callable): The Ez(k0,x,y,z) field
879
+ kz (Callable): The out of plane propagation constant kz(k0)
880
+ power (float): The port output power
881
+ """
882
+ super().__init__(face)
883
+ if cs is None:
884
+ cs = GCS
885
+
886
+ self.cs = cs
887
+ self.port_number: int= port_number
888
+ self.active: bool = False
889
+ self.power: float = power
890
+ self.type: str = 'TE'
891
+ if Ex is None:
892
+ Ex = _f_zero
893
+ if Ey is None:
894
+ Ey = _f_zero
895
+ if Ez is None:
896
+ Ez = _f_zero
897
+ if kz is None:
898
+ kz = lambda k0: k0
899
+
900
+ self._fex: Callable = Ex
901
+ self._fey: Callable = Ey
902
+ self._fez: Callable = Ez
903
+ self._fkz: Callable = kz
904
+ self.type = modetype
905
+
906
+ def get_basis(self) -> np.ndarray:
907
+ return self.cs._basis
908
+
909
+ def get_inv_basis(self) -> np.ndarray:
910
+ return self.cs._basis_inv
911
+
912
+ def modetype(self, k0):
913
+ return self.type
914
+
915
+ def get_amplitude(self, k0: float) -> float:
916
+ return np.sqrt(self.power)
917
+
918
+ def get_beta(self, k0: float) -> float:
919
+ ''' Return the out of plane propagation constant. βz.'''
920
+ return self._fkz(k0)
921
+
922
+ def get_gamma(self, k0: float) -> complex:
923
+ """Computes the γ-constant for matrix assembly. This constant is required for the Robin boundary condition.
924
+
925
+ Args:
926
+ k0 (float): The free space propagation constant.
927
+
928
+ Returns:
929
+ complex: The γ-constant
930
+ """
931
+ return 1j*self.get_beta(k0)
932
+
933
+ def get_Uinc(self, x_global: np.ndarray, y_global: np.ndarray, z_global: np.ndarray, k0: float) -> np.ndarray:
934
+ return -2*1j*self.get_beta(k0)*self.port_mode_3d_global(x_global, y_global, z_global, k0)
935
+
936
+ def port_mode_3d(self,
937
+ x_local: np.ndarray,
938
+ y_local: np.ndarray,
939
+ k0: float,
940
+ which: Literal['E','H'] = 'E') -> np.ndarray:
941
+ x_global, y_global, z_global = self.cs.in_global_cs(x_local, y_local, 0*x_local)
942
+
943
+ Egxyz = self.port_mode_3d_global(x_global,y_global,z_global,k0,which=which)
944
+
945
+ Ex, Ey, Ez = self.cs.in_local_basis(Egxyz[0,:], Egxyz[1,:], Egxyz[2,:])
946
+
947
+ Exyz = np.array([Ex, Ey, Ez])
948
+ return Exyz
949
+
950
+ def port_mode_3d_global(self,
951
+ x_global: np.ndarray,
952
+ y_global: np.ndarray,
953
+ z_global: np.ndarray,
954
+ k0: float,
955
+ which: Literal['E','H'] = 'E') -> np.ndarray:
956
+ '''Compute the port mode field for global xyz coordinates.'''
957
+ xl, yl, _ = self.cs.in_local_cs(x_global, y_global, z_global)
958
+ Ex = self._fex(k0, x_global, y_global, z_global)
959
+ Ey = self._fey(k0, x_global, y_global, z_global)
960
+ Ez = self._fez(k0, x_global, y_global, z_global)
961
+ Exg, Eyg, Ezg = self.cs.in_global_basis(Ex, Ey, Ez)
962
+ return np.array([Exg, Eyg, Ezg])
963
+
819
964
  class LumpedPort(PortBC):
820
965
 
821
966
  _include_stiff: bool = True
@@ -874,6 +1019,11 @@ class LumpedPort(PortBC):
874
1019
  self.vintline: list[Line] = []
875
1020
  self.v_integration = True
876
1021
 
1022
+ # Sanity checks
1023
+ if self.width > 0.5 or self.height > 0.5:
1024
+ DEBUG_COLLECTOR.add_report(f'{self}: A lumped port width/height larger than 0.5m has been detected: width={self.width:.3f}m. Height={self.height:.3f}.m. Perhaps you forgot a unit like mm, um, or mil')
1025
+
1026
+
877
1027
  @property
878
1028
  def surfZ(self) -> float:
879
1029
  """The surface sheet impedance for the lumped port
@@ -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,10 +784,10 @@ 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,
@@ -1146,6 +1187,54 @@ class MWScalarNdim:
1146
1187
  def S(self, i1: int, i2: int) -> np.ndarray:
1147
1188
  return self.Sp[...,self._portmap[i1], self._portmap[i2]]
1148
1189
 
1190
+ def combine_ports(self, p1: int, p2: int) -> MWScalarNdim:
1191
+ """Combine ports p1 and p2 into a differential and common mode port respectively.
1192
+
1193
+ The p1 index becomes the differential mode port
1194
+ The p2 index becomes the common mode port
1195
+
1196
+ Args:
1197
+ p1 (int): The first port number
1198
+ p2 (int): The second port number
1199
+
1200
+ Returns:
1201
+ MWScalarNdim: _description_
1202
+ """
1203
+ if p1==p2:
1204
+ raise ValueError('p1 and p2 must be different port numbers')
1205
+
1206
+ F, N, _ = self.Sp.shape
1207
+ p1 = p1-1
1208
+ p2 = p2-1
1209
+
1210
+ if not (0 <= p1 < N and 0 <= p2 < N):
1211
+ raise IndexError(f'Ports {p1+1} or {p2+1} are out of range {N}')
1212
+
1213
+ Sout = self.Sp.copy()
1214
+ ii, jj = p1, p2
1215
+ idx = np.ones(N, dtype=np.bool)
1216
+ idx[[ii,jj]] = False
1217
+ others = np.nonzero(idx)[0]
1218
+ isqrt2 = 1.0 / np.sqrt(2.0)
1219
+
1220
+ Sout[:, others, ii] = (self.Sp[:, others, ii] - self.Sp[:, others, jj]) * isqrt2
1221
+ Sout[:, others, jj] = (self.Sp[:, others, ii] + self.Sp[:, others, jj]) * isqrt2
1222
+ Sout[:, ii, others] = (self.Sp[:, ii, others] - self.Sp[:, jj, others]) * isqrt2
1223
+ Sout[:, jj, others] = (self.Sp[:, ii, others] + self.Sp[:, jj, others]) * isqrt2
1224
+
1225
+ Sii = self.Sp[:, ii, ii]
1226
+ Sij = self.Sp[:, ii, jj]
1227
+ Sji = self.Sp[:, jj, ii]
1228
+ Sjj = self.Sp[:, jj, jj]
1229
+
1230
+ Sout[:, ii, ii] = 0.5 *(Sii - Sij - Sji + Sjj)
1231
+ Sout[:, ii, jj] = 0.5 *(Sii + Sij - Sji - Sjj)
1232
+ Sout[:, jj, ii] = 0.5 *(Sii - Sij + Sji - Sjj)
1233
+ Sout[:, jj, jj] = 0.5 *(Sii + Sij + Sji + Sjj)
1234
+
1235
+ self.Sp = Sout
1236
+
1237
+ return self
1149
1238
  @property
1150
1239
  def Smat(self) -> np.ndarray:
1151
1240
  """Returns the full S-matrix
@@ -15,20 +15,21 @@
15
15
  # along with this program; if not, see
16
16
  # <https://www.gnu.org/licenses/>.
17
17
  from __future__ import annotations
18
- import time
19
18
  from ...mesh3d import Mesh3D
19
+ from ...simstate import SimState
20
20
  from ...geometry import GeoObject
21
21
  from ...selection import FaceSelection, DomainSelection, EdgeSelection, Selection, encode_data
22
22
  from ...physics.microwave.microwave_bc import PortBC, ModalPort
23
- import numpy as np
24
- import pyvista as pv
25
- from typing import Iterable, Literal, Callable, Any
26
23
  from ..display import BaseDisplay
27
24
  from .display_settings import PVDisplaySettings
28
- from matplotlib.colors import ListedColormap
29
25
  from .cmap_maker import make_colormap
30
26
 
27
+ import time
28
+ import numpy as np
29
+ import pyvista as pv
30
+ from typing import Iterable, Literal, Callable, Any
31
31
  from itertools import cycle
32
+ from loguru import logger
32
33
  ### Color scale
33
34
 
34
35
  # Define the colors we want to use
@@ -232,8 +233,8 @@ class _AnimObject:
232
233
 
233
234
  class PVDisplay(BaseDisplay):
234
235
 
235
- def __init__(self, mesh: Mesh3D):
236
- self._mesh: Mesh3D = mesh
236
+ def __init__(self, state: SimState):
237
+ self._state: SimState = state
237
238
  self.set: PVDisplaySettings = PVDisplaySettings()
238
239
 
239
240
  # Animation options
@@ -241,6 +242,7 @@ class PVDisplay(BaseDisplay):
241
242
  self._stop: bool = False
242
243
  self._objs: list[_AnimObject] = []
243
244
  self._do_animate: bool = False
245
+ self._animate_next: bool = False
244
246
  self._closed_via_x: bool = False
245
247
  self._Nsteps: int = 0
246
248
  self._fps: int = 25
@@ -260,6 +262,9 @@ class PVDisplay(BaseDisplay):
260
262
  self._cbar_lim: tuple[float, float] | None = None
261
263
  self.camera_position = (1, -1, 1) # +X, +Z, -Y
262
264
 
265
+ @property
266
+ def _mesh(self) -> Mesh3D:
267
+ return self._state.mesh
263
268
 
264
269
  def cbar(self, name: str, n_labels: int = 5, interactive: bool = False, clim: tuple[float, float] | None = None ) -> PVDisplay:
265
270
  self._cbar_args = dict(title=name, n_labels=n_labels, interactive=interactive)
@@ -302,8 +307,7 @@ class PVDisplay(BaseDisplay):
302
307
  self._ruler.min_length = max(1e-3, min(self._mesh.edge_lengths))
303
308
  self._update_camera()
304
309
  self._add_aux_items()
305
- # self._plot.renderer.enable_depth_peeling(20, 0.8)
306
- # self._plot.enable_anti_aliasing(self.set.anti_aliassing)
310
+ self._add_background()
307
311
  if self._do_animate:
308
312
  self._wire_close_events()
309
313
  self.add_text('Press Q to close!',color='red', position='upper_left')
@@ -313,14 +317,17 @@ class PVDisplay(BaseDisplay):
313
317
  self._plot.show()
314
318
 
315
319
  self._reset()
316
-
317
- def set_mesh(self, mesh: Mesh3D):
318
- """Define the mesh to be used
319
320
 
320
- Args:
321
- mesh (Mesh3D): The mesh object
322
- """
323
- self._mesh = mesh
321
+ def _add_background(self):
322
+ from pyvista import examples
323
+ from requests.exceptions import ConnectionError
324
+
325
+ try:
326
+ cubemap = examples.download_sky_box_cube_map()
327
+ self._plot.set_environment_texture(cubemap)
328
+ except ConnectionError:
329
+ logger.warning(f'No internet, no background texture will be used.')
330
+
324
331
 
325
332
  def _reset(self):
326
333
  """ Resets key display parameters."""
@@ -328,6 +335,7 @@ class PVDisplay(BaseDisplay):
328
335
  self._plot = pv.Plotter()
329
336
  self._stop = False
330
337
  self._objs = []
338
+ self._animate_next = False
331
339
  C_CYCLE = _gen_c_cycle()
332
340
 
333
341
  def _close_callback(self, arg):
@@ -397,6 +405,7 @@ class PVDisplay(BaseDisplay):
397
405
  print('If you closed the animation without using (Q) press Ctrl+C to kill the process.')
398
406
  self._Nsteps = Nsteps
399
407
  self._fps = fps
408
+ self._animate_next = True
400
409
  self._do_animate = True
401
410
  return self
402
411
 
@@ -456,6 +465,10 @@ class PVDisplay(BaseDisplay):
456
465
  ## OBLIGATORY METHODS
457
466
  def add_object(self, obj: GeoObject | Selection, mesh: bool = False, volume_mesh: bool = True, label: bool = False, *args, **kwargs):
458
467
 
468
+ if isinstance(obj, GeoObject):
469
+ if obj._hidden:
470
+ return
471
+
459
472
  show_edges = False
460
473
  opacity = obj.opacity
461
474
  line_width = 0.5
@@ -508,7 +521,7 @@ class PVDisplay(BaseDisplay):
508
521
 
509
522
  self._plot.add_mesh(self._volume_edges(_select(obj)), color='#000000', line_width=2, show_edges=True)
510
523
 
511
- if isinstance(obj, GeoObject) and label:
524
+ if label:
512
525
  points = []
513
526
  labels = []
514
527
  for dt in obj.dimtags:
@@ -681,14 +694,14 @@ class PVDisplay(BaseDisplay):
681
694
  actor = self._plot.add_mesh(grid_no_nan, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
682
695
 
683
696
 
684
- if self._do_animate:
697
+ if self._animate_next:
685
698
  def on_update(obj: _AnimObject, phi: complex):
686
699
  field_anim = obj.T(np.real(obj.field * phi))
687
700
  obj.grid[name] = field_anim
688
701
  obj.fgrid[name] = obj.grid.threshold(scalars=name)[name]
689
702
  #obj.fgrid replace with thresholded scalar data.
690
703
  self._objs.append(_AnimObject(field_flat, T, grid, grid_no_nan, actor, on_update))
691
-
704
+ self._animate_next = False
692
705
  self._reset_cbar()
693
706
 
694
707
  def add_boundary_field(self,
@@ -763,13 +776,13 @@ class PVDisplay(BaseDisplay):
763
776
  kwargs = setdefault(kwargs, cmap=cmap, clim=clim, opacity=opacity, pickable=False, multi_colors=True)
764
777
  actor = self._plot.add_mesh(grid, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
765
778
 
766
- if self._do_animate:
779
+ if self._animate_next:
767
780
  def on_update(obj: _AnimObject, phi: complex):
768
781
  field_anim = obj.T(np.real(obj.field * phi))
769
782
  obj.grid[name] = field_anim
770
783
  #obj.fgrid replace with thresholded scalar data.
771
784
  self._objs.append(_AnimObject(field_flat, T, grid, grid, actor, on_update))
772
-
785
+ self._animate_next = False
773
786
  self._reset_cbar()
774
787
 
775
788
  def add_title(self, title: str) -> None:
@@ -911,7 +924,7 @@ class PVDisplay(BaseDisplay):
911
924
 
912
925
  actor = self._plot.add_mesh(contour, opacity=opacity, cmap=cmap, clim=clim, pickable=False, scalar_bar_args=self._cbar_args)
913
926
 
914
- if self._do_animate:
927
+ if self._animate_next:
915
928
  def on_update(obj: _AnimObject, phi: complex):
916
929
  new_vals = obj.T(np.real(obj.field * phi))
917
930
  obj.grid['anim'] = new_vals
@@ -919,7 +932,7 @@ class PVDisplay(BaseDisplay):
919
932
  obj.actor.GetMapper().SetInputData(new_contour) # type: ignore
920
933
 
921
934
  self._objs.append(_AnimObject(field, T, grid, None, actor, on_update)) # type: ignore
922
-
935
+ self._animate_next = False
923
936
  self._reset_cbar()
924
937
 
925
938
  def _add_aux_items(self) -> None:
@@ -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