diff --git a/doc/changelog.d/6595.added.md b/doc/changelog.d/6595.added.md new file mode 100644 index 00000000000..c086228dc88 --- /dev/null +++ b/doc/changelog.d/6595.added.md @@ -0,0 +1 @@ +Waveport object advanced config diff --git a/src/ansys/aedt/core/application/analysis.py b/src/ansys/aedt/core/application/analysis.py index d14c1e011eb..37a8d81eed0 100644 --- a/src/ansys/aedt/core/application/analysis.py +++ b/src/ansys/aedt/core/application/analysis.py @@ -666,7 +666,7 @@ def design_excitations(self): >>> oModule.GetExcitations """ exc_names = self.excitation_names[::] - + exc_names = list(dict.fromkeys(s.split(":", 1)[0] for s in exc_names)) for el in self.boundaries: if el.name in exc_names: self._excitation_objects[el.name] = el @@ -675,7 +675,7 @@ def design_excitations(self): keys_to_remove = [ internal_excitation for internal_excitation in self._excitation_objects - if internal_excitation not in self.excitation_names + if internal_excitation not in self.excitation_names and internal_excitation not in exc_names ] for key in keys_to_remove: diff --git a/src/ansys/aedt/core/application/design.py b/src/ansys/aedt/core/application/design.py index 81796b8e911..13054b2e89e 100644 --- a/src/ansys/aedt/core/application/design.py +++ b/src/ansys/aedt/core/application/design.py @@ -85,6 +85,7 @@ from ansys.aedt.core.internal.errors import GrpcApiError from ansys.aedt.core.internal.load_aedt_file import load_entire_aedt_file from ansys.aedt.core.modules.boundary.common import BoundaryObject +from ansys.aedt.core.modules.boundary.hfss_boundary import WavePort from ansys.aedt.core.modules.boundary.icepak_boundary import NetworkObject from ansys.aedt.core.modules.boundary.layout_boundary import BoundaryObject3dLayout from ansys.aedt.core.modules.boundary.maxwell_boundary import MaxwellParameters @@ -511,6 +512,8 @@ def boundaries(self) -> List[BoundaryObject]: self._boundaries[boundary] = BoundaryObject(self, boundary, boundarytype=maxwell_motion_type) elif boundarytype == "Network": self._boundaries[boundary] = NetworkObject(self, boundary) + elif boundarytype == "Wave Port": + self._boundaries[boundary] = WavePort(self, boundary) else: self._boundaries[boundary] = BoundaryObject(self, boundary, boundarytype=boundarytype) diff --git a/src/ansys/aedt/core/hfss.py b/src/ansys/aedt/core/hfss.py index 998058efd98..4ca8b83ecb8 100644 --- a/src/ansys/aedt/core/hfss.py +++ b/src/ansys/aedt/core/hfss.py @@ -24,6 +24,7 @@ """This module contains the ``Hfss`` class.""" +from enum import Enum import math from pathlib import Path import tempfile @@ -48,6 +49,7 @@ from ansys.aedt.core.generic.numbers_utils import is_number from ansys.aedt.core.generic.settings import settings from ansys.aedt.core.internal.errors import AEDTRuntimeError +from ansys.aedt.core.internal.errors import GrpcApiError from ansys.aedt.core.mixins import CreateBoundaryMixin from ansys.aedt.core.modeler import cad from ansys.aedt.core.modeler.cad.component_array import ComponentArray @@ -56,10 +58,19 @@ from ansys.aedt.core.modules.boundary.common import BoundaryObject from ansys.aedt.core.modules.boundary.hfss_boundary import FarFieldSetup from ansys.aedt.core.modules.boundary.hfss_boundary import NearFieldSetup +from ansys.aedt.core.modules.boundary.hfss_boundary import WavePort from ansys.aedt.core.modules.boundary.layout_boundary import NativeComponentObject from ansys.aedt.core.modules.setup_templates import SetupKeys +class NearFieldType(str, Enum): + Sphere = "NearFieldSphere" + Box = "NearFieldBox" + Rectangle = "NearFieldRectangle" + Line = "NearFieldLine" + Points = "NearFieldPoints" + + class Hfss(FieldAnalysis3D, ScatteringMethods, CreateBoundaryMixin): """Provides the HFSS application interface. @@ -238,27 +249,40 @@ def _init_from_design(self, *args, **kwargs): self.__init__(*args, **kwargs) @pyaedt_function_handler - # NOTE: Extend Mixin behaviour to handle near field setups + # NOTE: Extend Mixin behaviour to handle HFSS excitations def _create_boundary(self, name, props, boundary_type): - # No-near field cases - if boundary_type not in ( - "NearFieldSphere", - "NearFieldBox", - "NearFieldRectangle", - "NearFieldLine", - "NearFieldPoints", + # Wave Port cases - return WavePort + if boundary_type == "Wave Port": + try: + bound = WavePort(self, name, props) + if not bound.create(): + raise AEDTRuntimeError(f"Failed to create boundary {boundary_type} {name}") + + self._boundaries[bound.name] = bound + self.logger.info(f"Boundary {boundary_type} {name} has been created.") + return bound + except GrpcApiError as e: + raise AEDTRuntimeError(f"Failed to create boundary {boundary_type} {name}") from e + + # Near field cases + if boundary_type in ( + NearFieldType.Sphere, + NearFieldType.Box, + NearFieldType.Rectangle, + NearFieldType.Line, + NearFieldType.Points, ): + # Near field setup + bound = NearFieldSetup(self, name, props, boundary_type) + result = bound.create() + if result: + self.field_setups.append(bound) + self.logger.info(f"Field setup {boundary_type} {name} has been created.") + return bound + raise AEDTRuntimeError(f"Failed to create near field setup {boundary_type} {name}") + else: return super()._create_boundary(name, props, boundary_type) - # Near field setup - bound = NearFieldSetup(self, name, props, boundary_type) - result = bound.create() - if result: - self.field_setups.append(bound) - self.logger.info(f"Field setup {boundary_type} {name} has been created.") - return bound - raise AEDTRuntimeError(f"Failed to create near field setup {boundary_type} {name}") - @property def field_setups(self): """List of AEDT radiation fields. @@ -5624,7 +5648,7 @@ def insert_near_field_sphere( props["CoordSystem"] = custom_coordinate_system else: props["CoordSystem"] = "" - return self._create_boundary(name, props, "NearFieldSphere") + return self._create_boundary(name, props, NearFieldType.Sphere) @pyaedt_function_handler() def insert_near_field_box( @@ -5695,7 +5719,7 @@ def insert_near_field_box( props["CoordSystem"] = custom_coordinate_system else: props["CoordSystem"] = "Global" - return self._create_boundary(name, props, "NearFieldBox") + return self._create_boundary(name, props, NearFieldType.Box) @pyaedt_function_handler() def insert_near_field_rectangle( @@ -5759,7 +5783,7 @@ def insert_near_field_rectangle( else: props["CoordSystem"] = "Global" - return self._create_boundary(name, props, "NearFieldRectangle") + return self._create_boundary(name, props, NearFieldType.Rectangle) @pyaedt_function_handler(line="assignment") def insert_near_field_line( @@ -5804,7 +5828,7 @@ def insert_near_field_line( props["NumPts"] = points props["Line"] = assignment - return self._create_boundary(name, props, "NearFieldLine") + return self._create_boundary(name, props, NearFieldType.Line) @pyaedt_function_handler() def insert_near_field_points( @@ -5842,7 +5866,7 @@ def insert_near_field_points( props["CoordSystem"] = coordinate_system props["PointListFile"] = str(point_file) - return self._create_boundary(name, props, "NearFieldPoints") + return self._create_boundary(name, props, NearFieldType.Points) @pyaedt_function_handler() def set_sbr_current_sources_options(self, conformance=False, thin_sources=False, power_fraction=0.95): diff --git a/src/ansys/aedt/core/modules/boundary/hfss_boundary.py b/src/ansys/aedt/core/modules/boundary/hfss_boundary.py index 7379a957e09..e95f70a2e9e 100644 --- a/src/ansys/aedt/core/modules/boundary/hfss_boundary.py +++ b/src/ansys/aedt/core/modules/boundary/hfss_boundary.py @@ -22,10 +22,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import Optional +from typing import Union + +from ansys.aedt.core.generic.constants import AEDT_UNITS from ansys.aedt.core.generic.data_handlers import _dict2arg from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.numbers_utils import Quantity +from ansys.aedt.core.internal.errors import AEDTRuntimeError from ansys.aedt.core.modeler.cad.elements_3d import BinaryTreeNode from ansys.aedt.core.modules.boundary.common import BoundaryCommon +from ansys.aedt.core.modules.boundary.common import BoundaryObject from ansys.aedt.core.modules.boundary.common import BoundaryProps @@ -529,3 +536,761 @@ class NearFieldSetup(FieldSetup, object): def __init__(self, app, component_name, props, component_type): FieldSetup.__init__(self, app, component_name, props, component_type) + + +class WavePort(BoundaryObject): + """Manages HFSS Wave Port boundary objects. + + This class provides specialized functionality for wave port + boundaries in HFSS, including analytical alignment settings. + + Examples + -------- + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss() + >>> wave_port = hfss.wave_port( + ... + ... ) + >>> wave_port.set_analytical_alignment(True) + """ + + def __init__(self, app, name, props=None): + """Initialize a wave port boundary object. + + Parameters + ---------- + app : :class:`ansys.aedt.core.application.analysis_3d.FieldAnalysis3D` + The AEDT application instance. + name : str + Name of the boundary. + props : dict, optional + Dictionary of boundary properties. + """ + super().__init__(app, name, props, "Wave Port") + + @property + def assignment(self) -> Union[str, int]: + """Wave port object assignment. + + Returns + ------- + str or int + Object assignment. + """ + value = None + if "Assignment" in self.properties: + value = self.properties["Assignment"] + if "Face_" in value: + value = [int(i.replace("Face_", "")) for i in value.split("(")[1].split(")")[0].split(",")][0] + return value + + @property + def modes(self) -> int: + """Number of modes. + + Returns + ------- + int + """ + value = None + if "Num Modes" in self.properties: + value = self.properties["Num Modes"] + return value + + @property + def specify_wave_direction(self) -> bool: + """Enable specify wave direction. + + Returns + ------- + bool + Whether the wave direction is specified. + """ + value = None + if "Specify Wave Direction" in self.properties: + value = self.properties["Specify Wave Direction"] + return value + + @specify_wave_direction.setter + def specify_wave_direction(self, value: bool): + if not isinstance(value, bool) or self.specify_wave_direction is None: + raise AEDTRuntimeError("Wave direction must be a boolean.") + self.properties["Specify Wave Direction"] = value + + @property + def wave_direction(self) -> list: + """Wave direction. + + Returns + ------- + list + Wave direction. + """ + value = None + if "Wave Direction" in self.properties: + value = list(self.properties["Wave Direction"]) + return value + + @property + def use_deembed(self) -> bool: + """Use dembedding. + + Returns + ------- + bool + Use dembedding. + """ + value = None + if "Deembed" in self.properties: + value = self.properties["Deembed"] + return value + + @use_deembed.setter + def use_deembed(self, value: bool): + if not isinstance(value, bool) or self.use_deembed is None: + raise AEDTRuntimeError("Use dembedding must be a boolean.") + self.properties["Deembed"] = value + + @property + def deembed(self) -> Union[Quantity, None]: + """Dembedding distance. + + Returns + ------- + Quantity or None + Whether de-embedding is enabled. + """ + value = None + if self.use_deembed: + value = Quantity(self.properties["Deembed Dist"]) + return value + + @deembed.setter + def deembed(self, value: Optional[Union[Quantity, float, int, str, bool]]): + if value is None or value is False: + self.use_deembed = False + elif value is True: + self.use_deembed = True + self.properties["Deembed Dist"] = str("0.0mm") + else: + if not self.use_deembed: + self.use_deembed = True + if not isinstance(value, Quantity): + value = Quantity(self._app.value_with_units(value)) + self.properties["Deembed Dist"] = str(value) + + @property + def renorm_all_modes(self) -> bool: + """Renormalize all modes. + + Returns + ------- + bool + Whether renormalization of all modes is enabled. + """ + value = None + if "Renorm All Modes" in self.properties: + value = self.properties["Renorm All Modes"] + return value + + @renorm_all_modes.setter + def renorm_all_modes(self, value: bool): + if not isinstance(value, bool) or self.renorm_all_modes is None: + raise AEDTRuntimeError("Renorm all modes must be a boolean.") + self.properties["Renorm All Modes"] = value + + @property + def renorm_impedance_type(self) -> str: + """Get the renormalization impedance type + + Returns + ------- + str + The type of renormalization impedance. + """ + value = None + if "Renorm Impedance Type" in self.properties: + value = self.properties["Renorm Impedance Type"] + return value + + @renorm_impedance_type.setter + def renorm_impedance_type(self, value: str): + # Activate renorm all modes + if not self.renorm_all_modes: + self.renorm_all_modes = True + + if self.renorm_impedance_type and ( + "Renorm Impedance Type/Choices" in self.properties + and value not in self.properties["Renorm Impedance Type/Choices"] + ): + raise ValueError( + f"Renorm Impedance Type must be one of {self.properties['Renorm Impedance Type/Choices']}." + ) + self.properties["Renorm Impedance Type"] = value + + @property + def renorm_impedance(self) -> Union[Quantity, None]: + """Get the renormalization impedance value. + + Returns + ------- + Quantity + The renormalization impedance value. + """ + value = None + if "Renorm Imped" in self.properties: + value = Quantity(self.properties["Renorm Imped"]) + return value + + @renorm_impedance.setter + def renorm_impedance(self, value: Optional[Union[Quantity, float, int, str]]): + if self.renorm_impedance_type != "Impedance": + raise ValueError("Renorm Impedance can be set only if Renorm Impedance Type is 'Impedance'.") + + if not isinstance(value, Quantity): + value = Quantity(self._app.value_with_units(value, units_system="Resistance")) + + if value.unit not in AEDT_UNITS["Resistance"]: + raise ValueError("Renorm Impedance must have resistance units.") + self.properties["Renorm Imped"] = str(value) + + @property + def rlc_type(self) -> str: + """Get the RLC type property. + + Returns + ------- + str + RLC type. + """ + value = None + if "RLC Type" in self.properties: + value = self.properties["RLC Type"] + return value + + @rlc_type.setter + def rlc_type(self, value): + if self.renorm_impedance_type != "RLC": + raise ValueError("RLC Type can be set only if Renorm Impedance Type is 'RLC'.") + allowed_types = ["Serial", "Parallel"] + if value not in allowed_types: + raise ValueError(f"RLC Type must be one of {allowed_types}.") + self.properties["RLC Type"] = value + + @property + def use_resistance(self) -> bool: + """Use resistance. + + Returns + ------- + bool + Use resistance. + """ + # This property can not be disabled + value = None + if "Use Resistance" in self.properties: + value = self.properties["Use Resistance"] + return value + + @property + def resistance(self) -> Quantity: + """Resistance value. + + Returns + ------- + Quantity or None + Resistance value. + """ + value = None + if self.use_resistance: + value = Quantity(self.properties["Resistance Value"]) + return value + + @resistance.setter + def resistance(self, value: Optional[Union[Quantity, float, int, str]]): + if self.renorm_impedance_type == "RLC": + if not isinstance(value, Quantity): + value = Quantity(self._app.value_with_units(value, units_system="Resistance")) + if value.unit not in AEDT_UNITS["Resistance"]: + raise ValueError("Renorm Impedance must have resistance units.") + self.properties["Resistance Value"] = str(value) + + @property + def use_inductance(self) -> bool: + """Use inductance. + + Returns + ------- + bool + Use inductance. + """ + value = None + if "Use Inductance" in self.properties: + value = self.properties["Use Inductance"] + return value + + @use_inductance.setter + def use_inductance(self, value: bool): + if not isinstance(value, bool) or self.use_inductance is None: + raise AEDTRuntimeError("Use inductance must be a boolean.") + self.properties["Use Inductance"] = value + + @property + def inductance(self) -> Quantity: + """Inductance value. + + Returns + ------- + Quantity or None + Inductance value. + """ + value = None + if self.use_inductance: + value = Quantity(self.properties["Use Inductance"]) + return value + + @inductance.setter + def inductance(self, value: Optional[Union[Quantity, float, int, str, bool]]): + if value is None or value is False: + self.use_inductance = False + elif value is True: + self.use_inductance = True + self.properties["Inductance value"] = str("0.0nH") + else: + if not self.use_inductance: + self.use_inductance = True + + if not isinstance(value, Quantity): + value = Quantity(self._app.value_with_units(value, units_system="Inductance")) + if value.unit not in AEDT_UNITS["Inductance"]: + raise ValueError("Inductance must have inductance units.") + + self.properties["Inductance value"] = str(value) + + @property + def use_capacitance(self) -> bool: + """Use capacitance. + + Returns + ------- + bool + Use capacitance. + """ + value = None + if "Use Capacitance" in self.properties: + value = self.properties["Use Capacitance"] + return value + + @use_capacitance.setter + def use_capacitance(self, value: bool): + if not isinstance(value, bool) or self.use_capacitance is None: + raise AEDTRuntimeError("Use capacitance must be a boolean.") + self.properties["Use Capacitance"] = value + + @property + def capacitance(self) -> Quantity: + """Capacitance value. + + Returns + ------- + Quantity or None + Capacitance value. + """ + value = None + if self.use_capacitance: + value = Quantity(self.properties["Use Capacitance"]) + return value + + @capacitance.setter + def capacitance(self, value: Optional[Union[Quantity, float, int, str, bool]]): + if value is None or value is False: + self.use_capacitance = False + elif value is True: + self.use_capacitance = True + self.properties["Capactiance value"] = str("0.0nF") + else: + if not self.use_capacitance: + self.use_capacitance = True + + if not isinstance(value, Quantity): + value = Quantity(self._app.value_with_units(value, units_system="Capacitance")) + if value.unit not in AEDT_UNITS["Capacitance"]: + raise ValueError("Capacitance must have inductance units.") + + self.properties["Capacitance value"] = str(value) + + @property + def filter_modes_reporter(self): + """Get the reporter filter setting for each mode. + + Returns + ------- + list of bool + List of boolean values indicating whether each mode is + filtered in the reporter. + """ + return self.props["ReporterFilter"] + + @filter_modes_reporter.setter + def filter_modes_reporter(self, value): + """Set the reporter filter setting for wave port modes. + + Parameters + ---------- + value : bool or list of bool + Boolean value(s) to set for the reporter filter. If a + single boolean is provided, it will be applied to all + modes. If a list is provided, it must match the number + of modes. + + Examples + -------- + >>> # Set all modes to be filtered + >>> wave_port.filter_modes_reporter = True + + >>> # Set specific filter values for each mode + >>> wave_port.filter_modes_reporter = [True, False, True] + """ + try: + num_modes = self.properties["Num Modes"] + show_reporter_filter = True + if isinstance(value, bool): + # Single boolean value - apply to all modes + filter_values = [value] * num_modes + # In case all values are the same, we hide the Reporter Filter + show_reporter_filter = False + elif isinstance(value, list): + # List of boolean values + if not all(isinstance(v, bool) for v in value): + raise ValueError("All values in the list must be boolean.") + if len(value) != num_modes: + raise ValueError(f"List length ({len(value)}) must match the number of modes ({num_modes}).") + filter_values = value + else: + raise ValueError("Value must be a boolean or a list of booleans.") + self.props["ShowReporterFilter"] = show_reporter_filter + # Apply the filter values to each mode + self.props["ReporterFilter"] = filter_values + + self.update() + except Exception as e: + self._app.logger.error(f"Failed to set filter modes reporter: {str(e)}") + raise + + @pyaedt_function_handler() + def set_analytical_alignment( + self, u_axis_line=None, analytic_reverse_v=False, coordinate_system="Global", alignment_group=None + ): + """Set the analytical alignment property for the wave port. + + Parameters + ---------- + u_axis_line : list + List containing start and end points for the U-axis line. + Format: [[x1, y1, z1], [x2, y2, z2]] + analytic_reverse_v : bool, optional + Whether to reverse the V direction. Default is False. + coordinate_system : str, optional + Coordinate system to use. Default is "Global". + alignment_group : int, list or None, optional + Alignment group number(s) for the wave port. If None, the default group is used. + If a single integer is provided, it is applied to all modes. + + Returns + ------- + bool + True if the operation was successful, False otherwise. + + Examples + -------- + >>> u_line = [[0, 0, 0], [1, 0, 0]] + >>> wave_port.set_analytical_alignment(u_line, analytic_reverse_v=True) + True + """ + try: + # Go through all modes and set the alignment group if provided + if alignment_group is None: + alignment_group = [0] * len(self.props["Modes"]) + elif isinstance(alignment_group, int): + alignment_group = [alignment_group] * len(self.props["Modes"]) + elif not (isinstance(alignment_group, list) and all(isinstance(x, (int, float)) for x in alignment_group)): + raise ValueError("alignment_group must be a list of numbers or None.") + if len(alignment_group) != len(self.props["Modes"]): + raise ValueError("alignment_group length must match the number of modes.") + if not ( + isinstance(u_axis_line, list) + and len(u_axis_line) == 2 + and all(isinstance(pt, list) and len(pt) == 3 for pt in u_axis_line) + ): + raise ValueError("u_axis_line must be a list of two 3-element lists.") + if not isinstance(analytic_reverse_v, bool): + raise ValueError("analytic_reverse_v must be a boolean.") + if not isinstance(coordinate_system, str): + raise ValueError("coordinate_system must be a string.") + + for i, mode_key in enumerate(self.props["Modes"]): + self.props["Modes"][mode_key]["AlignmentGroup"] = i + analytic_u_line = {} + analytic_u_line["Coordinate System"] = coordinate_system + analytic_u_line["Start"] = [str(i) + self._app.modeler.model_units for i in u_axis_line[0]] + analytic_u_line["End"] = [str(i) + self._app.modeler.model_units for i in u_axis_line[1]] + self.props["AnalyticULine"] = analytic_u_line + self.props["AnalyticReverseV"] = analytic_reverse_v + self.props["UseAnalyticAlignment"] = True + return self.update() + except Exception as e: + self._app.logger.error(f"Failed to set analytical alignment: {str(e)}") + return False + + @pyaedt_function_handler() + def set_alignment_integration_line(self, integration_lines=None, coordinate_system="Global", alignment_groups=None): + """Set the integration line alignment property for the wave port modes. + + This method configures integration lines for wave port modes, + which are used for modal excitation and field calculation. At + least the first 2 modes should have integration lines defined, + and at least 1 alignment group should exist. + + Parameters + ---------- + integration_lines : list of lists, optional + List of integration lines for each mode. Each integration + line is defined as [[start_x, start_y, start_z], + [end_x, end_y, end_z]]. If None, integration lines will + be disabled for all modes. + Format: [[[x1, y1, z1], [x2, y2, z2]], [[x3, y3, z3], ...]] + coordinate_system : str, optional + Coordinate system to use for the integration lines. + Default is "Global". + alignment_groups : list of int, optional + Alignment group numbers for each mode. If None, default + groups will be assigned. At least one alignment group + should exist. If a single integer is provided, it will be + applied to all modes with integration lines. + + Returns + ------- + bool + True if the operation was successful, False otherwise. + + Examples + -------- + >>> # Define integration lines for first two modes + >>> int_lines = [ + ... [[0, 0, 0], [10, 0, 0]], # Mode 1 integration line + ... [[0, 0, 0], [0, -9, 0]], # Mode 2 integration line + ... ] + >>> # Mode 1 in group 1, Mode 2 in group 0 + >>> alignment_groups = [1, 0] + >>> wave_port.set_alignment_integration_line(int_lines, "Global", alignment_groups) + True + + >>> # Disable integration lines for all modes + >>> wave_port.set_alignment_integration_line() + True + """ + try: + num_modes = self.properties["Num Modes"] + + if integration_lines is None: + # Disable integration lines for all modes + for mode_key in self.props["Modes"]: + self.props["Modes"][mode_key]["UseIntLine"] = False + if "IntLine" in self.props["Modes"][mode_key]: + del self.props["Modes"][mode_key]["IntLine"] + return self.update() + + # Validate integration_lines parameter + if not isinstance(integration_lines, list): + raise ValueError("integration_lines must be a list of integration line definitions.") + + # Ensure at least the first 2 modes have integration lines + if len(integration_lines) < min(2, num_modes): + raise ValueError("At least the first 2 modes should have integration lines defined.") + + # Validate each integration line format + for i, line in enumerate(integration_lines): + if not ( + isinstance(line, list) + and len(line) == 2 + and all(isinstance(pt, list) and len(pt) == 3 for pt in line) + ): + raise ValueError( + f"Integration line {i + 1} must be a list of two 3-element lists [[x1,y1,z1], [x2,y2,z2]]." + ) + + # Validate coordinate_system + if not isinstance(coordinate_system, str): + raise ValueError("coordinate_system must be a string.") + + # Handle alignment_groups parameter + if alignment_groups is None: + # Default: modes with integration lines get group 1, + # others get group 0 + alignment_groups = [1 if i < len(integration_lines) else 0 for i in range(num_modes)] + elif isinstance(alignment_groups, int): + # Single group for modes with integration lines + alignment_groups = [(alignment_groups if i < len(integration_lines) else 0) for i in range(num_modes)] + elif isinstance(alignment_groups, list): + # Validate alignment_groups list + if not all(isinstance(x, (int, float)) for x in alignment_groups): + raise ValueError("alignment_groups must be a list of integers.") + # Extend or truncate to match number of modes + if len(alignment_groups) < num_modes: + alignment_groups.extend([0] * (num_modes - len(alignment_groups))) + elif len(alignment_groups) > num_modes: + alignment_groups = alignment_groups[:num_modes] + else: + raise ValueError("alignment_groups must be an integer, list of integers, or None.") + + # Ensure at least one alignment group exists (non-zero) + if all(group == 0 for group in alignment_groups[: len(integration_lines)]): + self._app.logger.warning( + "No non-zero alignment groups defined. Setting first mode to alignment group 1." + ) + if len(alignment_groups) > 0: + alignment_groups[0] = 1 + + # Configure each mode + + mode_keys = list(self.props["Modes"].keys()) + for i, mode_key in enumerate(mode_keys): + # Set alignment group + self.props["Modes"][mode_key]["AlignmentGroup"] = alignment_groups[i] + if i < len(integration_lines): + # Mode has an integration line + + # Create IntLine structure + int_line = { + "Coordinate System": coordinate_system, + "Start": [f"{coord}{self._app.modeler.model_units}" for coord in integration_lines[i][0]], + "End": [f"{coord}{self._app.modeler.model_units}" for coord in integration_lines[i][1]], + } + self.props["Modes"][mode_key]["IntLine"] = int_line + self.props["Modes"][mode_key]["UseIntLine"] = True + else: + # Mode does not have an integration line + self.props["Modes"][mode_key]["UseIntLine"] = False + if "IntLine" in self.props["Modes"][mode_key]: + del self.props["Modes"][mode_key]["IntLine"] + self.props["UseLineModeAlignment"] = True + return self.update() + except Exception as e: + self._app.logger.error(f"Failed to set integration line alignment: {str(e)}") + return False + + @pyaedt_function_handler() + def set_polarity_integration_line(self, integration_lines=None, coordinate_system="Global"): + """Set polarity integration lines for the wave port modes. + + This method configures integration lines for wave port modes + with polarity alignment. When integration lines are provided, + they are used to define the field polarization direction for + each mode. The alignment mode is set to polarity + (UseLineModeAlignment=False). + + Parameters + ---------- + integration_lines : list of lists, optional + List of integration lines for each mode. Each integration + line is defined as [[start_x, start_y, start_z], + [end_x, end_y, end_z]]. If None, integration lines will + be disabled for all modes. + Format: [[[x1, y1, z1], [x2, y2, z2]], [[x3, y3, z3], ...]] + coordinate_system : str, optional + Coordinate system to use for the integration lines. + Default is "Global". + + Returns + ------- + bool + True if the operation was successful, False otherwise. + + Examples + -------- + >>> # Define integration lines for modes + >>> int_lines = [ + ... [[0, 0, 0], [10, 0, 0]], # Mode 1 integration line + ... [[0, 0, 0], [0, 10, 0]], # Mode 2 integration line + ... ] + >>> wave_port.set_polarity_integration_line(int_lines, "Global") + True + + >>> # Disable integration lines for all modes + >>> wave_port.set_polarity_integration_line() + True + """ + try: + # Set UseLineModeAlignment to False for polarity mode + self.props["UseLineModeAlignment"] = False + self.props["UseAnalyticAlignment"] = False + + if integration_lines is None: + return self.update() + + # Normalize integration_lines to handle single mode case + if not isinstance(integration_lines, list): + raise ValueError("integration_lines must be a list of integration line definitions.") + + # If not a list of lists, assume it's a single integration + # line for first mode + if len(integration_lines) > 0 and not isinstance(integration_lines[0], list): + integration_lines = [integration_lines] + elif ( + len(integration_lines) > 0 + and len(integration_lines[0]) == 2 + and isinstance(integration_lines[0][0], (int, float)) + ): + integration_lines = [integration_lines] + + # Validate each integration line format + for i, line in enumerate(integration_lines): + if not ( + isinstance(line, list) + and len(line) == 2 + and all(isinstance(pt, list) and len(pt) == 3 for pt in line) + ): + raise ValueError( + f"Integration line {i + 1} must be a list of two 3-element lists [[x1,y1,z1], [x2,y2,z2]]." + ) + + # Validate coordinate_system + if not isinstance(coordinate_system, str): + raise ValueError("coordinate_system must be a string.") + + # Configure each mode + mode_keys = list(self.props["Modes"].keys()) + for i, mode_key in enumerate(mode_keys): + # Set AlignmentGroup to 0 for polarity mode + mode_props = self.props["Modes"][mode_key] + mode_props["AlignmentGroup"] = 0 + + if i < len(integration_lines): + # Mode has an integration line + start = [ + (str(coord) + self._app.modeler.model_units if isinstance(coord, (int, float)) else coord) + for coord in integration_lines[i][0] + ] + stop = [ + (str(coord) + self._app.modeler.model_units if isinstance(coord, (int, float)) else coord) + for coord in integration_lines[i][1] + ] + + # Create IntLine structure + int_line = { + "Coordinate System": coordinate_system, + "Start": start, + "End": stop, + } + mode_props["IntLine"] = int_line + mode_props["UseIntLine"] = True + else: + # Mode does not have an integration line + mode_props["UseIntLine"] = False + if "IntLine" in mode_props: + del mode_props["IntLine"] + + return self.update() + except Exception as e: + self._app.logger.error(f"Failed to set polarity integration lines: {str(e)}") + return False diff --git a/tests/system/general/test_hfss_excitations.py b/tests/system/general/test_hfss_excitations.py new file mode 100644 index 00000000000..d2b2c0788ac --- /dev/null +++ b/tests/system/general/test_hfss_excitations.py @@ -0,0 +1,682 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Tests for HFSS excitations, particularly WavePort functionality.""" + +import pytest + +from ansys.aedt.core import Hfss +from ansys.aedt.core.generic.constants import Plane + + +class TestHfssWavePortExcitations: + """Test cases for HFSS Wave Port excitations.""" + + @classmethod + def setup_class(cls): + """Setup the HFSS application and waveguide once for all tests.""" + # Use pytest's request fixture to get add_app + # This will be set in the first test via setup method + cls.aedtapp = None + cls.input_face = None + cls.output_face = None + + @pytest.fixture(autouse=True) + def setup(self, add_app): + """Setup the HFSS application for testing, only once.""" + if TestHfssWavePortExcitations.aedtapp is None: + TestHfssWavePortExcitations.aedtapp = add_app(application=Hfss, solution_type="Modal") + # Create a simple waveguide structure for testing + box = TestHfssWavePortExcitations.aedtapp.modeler.create_box([0, 0, 0], [50, 5, 10], name="waveguide") + box.material_name = "vacuum" + TestHfssWavePortExcitations.input_face = TestHfssWavePortExcitations.aedtapp.modeler.create_rectangle( + Plane.YZ, [0, 0, 0], [5, 10], name="input_face" + ) + TestHfssWavePortExcitations.output_face = TestHfssWavePortExcitations.aedtapp.modeler.create_rectangle( + Plane.YZ, [50, 0, 0], [5, 10], name="output_face" + ) + self.aedtapp = TestHfssWavePortExcitations.aedtapp + self.input_face = TestHfssWavePortExcitations.input_face + self.output_face = TestHfssWavePortExcitations.output_face + + def test_01_create_wave_port_basic(self): + """Test basic wave port creation.""" + port = self.aedtapp.wave_port(assignment=self.input_face.name, name="test_port_basic") + assert port is not None + assert port.name == "test_port_basic" + assert hasattr(port, "specify_wave_direction") + assert hasattr(port, "deembed") + assert hasattr(port, "renorm_all_modes") + + def test_02_specify_wave_direction_property(self): + """Test specify_wave_direction property setter and getter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_wave_direction", + ) + + # Test getter + initial_value = port.specify_wave_direction + assert isinstance(initial_value, bool) + + # Test setter - True + port.specify_wave_direction = True + assert port.specify_wave_direction is True + + # Test setter - False + port.specify_wave_direction = False + assert port.specify_wave_direction is False + + # Test no change when setting same value + result = port.specify_wave_direction = False + assert result is False + + def test_03_deembed_property(self): + """Test deembed property setter and getter.""" + port = self.aedtapp.wave_port(assignment=self.input_face.name, name="test_port_deembed") + + # Test getter + initial_value = port.deembed + assert isinstance(initial_value, bool) + + # Test setter - True + port.deembed = True + assert port.deembed is True + + # Test setter - False + port.deembed = False + assert port.deembed is False + + # Test no change when setting same value + result = port.deembed = False + assert result is False + + def test_04_renorm_all_modes_property(self): + """Test renorm_all_modes property setter and getter.""" + port = self.aedtapp.wave_port(assignment=self.input_face.name, name="test_port_renorm") + + # Test getter + initial_value = port.renorm_all_modes + assert isinstance(initial_value, bool) + + # Test setter - True + port.renorm_all_modes = True + assert port.renorm_all_modes is True + + # Test setter - False + port.renorm_all_modes = False + assert port.renorm_all_modes is False + + # Test no change when setting same value + result = port.renorm_all_modes = False + assert result is False + + def test_05_renorm_impedance_type_property(self): + """Test renorm_impedance_type property setter and getter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_impedance_type", + ) + + # Test getter + initial_type = port.renorm_impedance_type + assert isinstance(initial_type, str) + + # Test valid values from choices + choices = port.properties["Renorm Impedance Type/Choices"] + for choice in choices: + port.renorm_impedance_type = choice + assert port.renorm_impedance_type == choice + + # Test invalid value + with pytest.raises(ValueError, match="Renorm Impedance Type must be one of"): + port.renorm_impedance_type = "InvalidType" + + # Test no change when setting same value + port.renorm_impedance_type = "Impedance" + result = port.renorm_impedance_type = "Impedance" + assert result == "Impedance" + + def test_06_renorm_impedance_property(self): + """Test renorm_impedance property setter and getter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_impedance", + ) + + # Set renorm type to Impedance first + port.renorm_impedance_type = "Impedance" + + # Test setter with int + port.renorm_impedance = 75 + assert port.renorm_impedance == "75ohm" + + # Test setter with string with units + port.renorm_impedance = "100ohm" + assert port.renorm_impedance == "100ohm" + + port.renorm_impedance = "1kOhm" + assert port.renorm_impedance == "1kOhm" + + # Test invalid units + with pytest.raises(ValueError, match="must end with one of"): + port.renorm_impedance = "50invalid" + + # Test invalid type + with pytest.raises(ValueError, match="must be a string with units or a float"): + port.renorm_impedance = [] + + # Test error when renorm type is not Impedance + port.renorm_impedance_type = "RLC" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'Impedance'", + ): + port.renorm_impedance = 50 + + def test_07_rlc_type_property(self): + """Test rlc_type property setter.""" + port = self.aedtapp.wave_port(assignment=self.input_face.name, name="test_port_rlc_type") + + # Set renorm type to RLC first + port.renorm_impedance_type = "RLC" + + # Test valid values + port.rlc_type = "Serial" + port.rlc_type = "Parallel" + + # Test invalid value + with pytest.raises(ValueError, match="RLC Type must be one of"): + port.rlc_type = "Invalid" + + # Test error when renorm type is not RLC + port.renorm_impedance_type = "Impedance" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'RLC'", + ): + port.rlc_type = "Serial" + + # Test getter raises NotImplementedError + port.renorm_impedance_type = "RLC" + with pytest.raises(NotImplementedError): + _ = port.rlc_type + + def test_08_use_resistance_property(self): + """Test use_resistance property setter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_use_resistance", + modes=8, + characteristic_impedance="Zwave", + renormalize=True, + ) + + # Set renorm type to RLC first + port.renorm_all_modes = True + port.renorm_impedance_type = "RLC" + + # Test error when renorm type is not RLC + port.renorm_impedance_type = "Impedance" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'RLC'", + ): + port.use_resistance = True + + # Test valid boolean values + port.renorm_impedance_type = "RLC" + port.use_resistance = True + port.use_resistance = False + + # Test invalid value + with pytest.raises(ValueError, match="must be a boolean value"): + port.use_resistance = "True" + # Test getter raises NotImplementedError + port.renorm_all_modes = True + port.renorm_impedance_type = "RLC" + with pytest.raises(NotImplementedError): + _ = port.use_resistance + + def test_09_resistance_value_property(self): + """Test resistance_value property setter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_resistance_value", + ) + + # Set renorm type to RLC first + port.renorm_impedance_type = "RLC" + + # Test setter with float + port.resistance_value = 50.0 + + # Test setter with int + port.resistance_value = 75 + + # Test setter with string with units + port.resistance_value = "100ohm" + port.resistance_value = "1kOhm" + + # Test invalid units + with pytest.raises(ValueError, match="must end with one of"): + port.resistance_value = "50invalid" + + # Test invalid type + with pytest.raises(ValueError, match="must be a string with units or a float"): + port.resistance_value = [] + + # Test error when renorm type is not RLC + port.renorm_impedance_type = "Impedance" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'RLC'", + ): + port.resistance_value = 50 + + # Test getter raises NotImplementedError + port.renorm_impedance_type = "RLC" + with pytest.raises(NotImplementedError): + _ = port.resistance_value + + def test_10_use_inductance_property(self): + """Test use_inductance property setter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_use_inductance", + ) + + # Set renorm type to RLC first + port.renorm_impedance_type = "RLC" + + # Test valid boolean values + port.use_inductance = True + port.use_inductance = False + + # Test invalid value + with pytest.raises(ValueError, match="must be a boolean value"): + port.use_inductance = "True" + + # Test error when renorm type is not RLC + port.renorm_impedance_type = "Impedance" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'RLC'", + ): + port.use_inductance = True + + # Test getter raises NotImplementedError + port.renorm_impedance_type = "RLC" + with pytest.raises(NotImplementedError): + _ = port.use_inductance + + def test_11_inductance_value_property(self): + """Test inductance_value property setter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_inductance_value", + ) + + # Set renorm type to RLC first + port.renorm_impedance_type = "RLC" + + # Test setter with float + port.inductance_value = 1e-9 + + # Test setter with int + port.inductance_value = 1 + + # Test setter with string with units + port.inductance_value = "10nH" + port.inductance_value = "1uH" + + # Test invalid units + with pytest.raises(ValueError, match="must end with one of"): + port.inductance_value = "10invalid" + + # Test invalid type + with pytest.raises(ValueError, match="must be a string with units or a float"): + port.inductance_value = [] + + # Test error when renorm type is not RLC + port.renorm_impedance_type = "Impedance" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'RLC'", + ): + port.inductance_value = 1e-9 + + # Test getter raises NotImplementedError + port.renorm_impedance_type = "RLC" + with pytest.raises(NotImplementedError): + _ = port.inductance_value + + def test_12_use_capacitance_property(self): + """Test use_capacitance property setter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_use_capacitance", + ) + + # Set renorm type to RLC first + port.renorm_impedance_type = "RLC" + + # Test valid boolean values + port.use_capacitance = True + port.use_capacitance = False + + # Test invalid value + with pytest.raises(ValueError, match="must be a boolean value"): + port.use_capacitance = "True" + + # Test error when renorm type is not RLC + port.renorm_impedance_type = "Impedance" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'RLC'", + ): + port.use_capacitance = True + + # Test getter raises NotImplementedError + port.renorm_impedance_type = "RLC" + with pytest.raises(NotImplementedError): + _ = port.use_capacitance + + def test_13_capacitance_value_property(self): + """Test capacitance_value property setter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_capacitance_value", + ) + + # Set renorm type to RLC first + port.renorm_impedance_type = "RLC" + + # Test setter with float + port.capacitance_value = 1e-12 + + # Test setter with int + port.capacitance_value = 1 + + # Test setter with string with units + port.capacitance_value = "1pF" + port.capacitance_value = "10nF" + + # Test invalid units + with pytest.raises(ValueError, match="must end with one of"): + port.capacitance_value = "1invalid" + + # Test invalid type + with pytest.raises(ValueError, match="must be a string with units or a float"): + port.capacitance_value = [] + + # Test error when renorm type is not RLC + port.renorm_impedance_type = "Impedance" + with pytest.raises( + ValueError, + match="can be set only if Renorm Impedance Type is 'RLC'", + ): + port.capacitance_value = 1e-12 + + # Test getter raises NotImplementedError + port.renorm_impedance_type = "RLC" + with pytest.raises(NotImplementedError): + _ = port.capacitance_value + + def test_14_filter_modes_reporter_property(self): + """Test filter_modes_reporter property setter and getter.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + modes=3, + name="test_port_filter_modes", + ) + + # Test getter + filter_values = port.filter_modes_reporter + assert isinstance(filter_values, list) + assert len(filter_values) == 3 + + # Test setter with single boolean + port.filter_modes_reporter = True + assert all(port.filter_modes_reporter) + + port.filter_modes_reporter = False + assert not any(port.filter_modes_reporter) + + # Test setter with list + port.filter_modes_reporter = [True, False, True] + expected = [True, False, True] + assert port.filter_modes_reporter == expected + + # Test invalid list length + with pytest.raises(ValueError, match="must match the number of modes"): + port.filter_modes_reporter = [True, False] + + # Test invalid type + with pytest.raises( + ValueError, + match="must be a boolean or a list of booleans", + ): + port.filter_modes_reporter = "True" + + def test_15_set_analytical_alignment(self): + """Test set_analytical_alignment method.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + name="test_port_analytical", + ) + + # Test with u_axis_line + u_line = [[0, 2.5, 0], [0, 2.5, 10]] + result = port.set_analytical_alignment(u_axis_line=u_line) + assert result is True + + # Test with all parameters + result = port.set_analytical_alignment( + u_axis_line=u_line, + analytic_reverse_v=True, + coordinate_system="Global", + alignment_group=1, + ) + assert result is True + + # Test with invalid u_axis_line format + result = port.set_analytical_alignment(u_axis_line=[[0, 0], [1, 0]]) + assert result is False + + # Test with invalid u_axis_line type + result = port.set_analytical_alignment(u_axis_line="invalid") + assert result is False + + def test_16_set_alignment_integration_line(self): + """Test set_alignment_integration_line method.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + modes=3, + name="test_port_alignment_integration", + ) + + # Test disabling integration lines + result = port.set_alignment_integration_line() + assert result is True + + # Test with valid integration lines + integration_lines = [ + [[0, 0, 0], [0, 5, 0]], + [[0, 0, 0], [0, 1, 0]], + ] + result = port.set_alignment_integration_line(integration_lines) + assert result is True + + # Test with alignment groups + alignment_groups = [1, 2, 0] + result = port.set_alignment_integration_line(integration_lines, alignment_groups=alignment_groups) + assert result is True + + # Test with single alignment group + result = port.set_alignment_integration_line(integration_lines, alignment_groups=1) + assert result is True + + # Test with custom coordinate system + result = port.set_alignment_integration_line(integration_lines, coordinate_system="Local") + assert result is True + + # Test error cases + # Not enough integration lines + result = port.set_alignment_integration_line([[[0, 0, 0], [1, 0, 0]]]) + assert result is False + + # Invalid integration line format + result = port.set_alignment_integration_line([[[0, 0], [1, 0]]]) + assert result is False + + # Invalid coordinate system + result = port.set_alignment_integration_line(integration_lines, coordinate_system=123) + assert result is False + + def test_17_set_polarity_integration_line(self): + """Test set_polarity_integration_line method.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + modes=2, + name="test_port_polarity", + ) + + # Test disabling integration lines + result = port.set_polarity_integration_line() + assert result is True + + # Test with valid integration lines + integration_lines = [ + [[0, 0, 0], [0, 5, 0]], + [[0, 0, 0], [0, 1, 0]], + ] + result = port.set_polarity_integration_line(integration_lines) + assert result is True + + # Test with custom coordinate system + result = port.set_polarity_integration_line(integration_lines, coordinate_system="Local") + assert result is True + + # Test single integration line handling + single_line = [[0, 0, 0], [0, 5, 0]] + result = port.set_polarity_integration_line([single_line]) + assert result is True + + # Test error cases + # Invalid integration line format + result = port.set_polarity_integration_line([[[0, 0], [1, 0]]]) + assert result is False + + # Invalid coordinate system + result = port.set_polarity_integration_line(integration_lines, coordinate_system=123) + assert result is False + + # Invalid integration_lines type + result = port.set_polarity_integration_line("invalid") + assert result is False + + def test_18_multiple_ports_properties(self): + """Test properties with multiple ports to ensure independence.""" + # Create two ports + port1 = self.aedtapp.wave_port(assignment=self.input_face.name, name="test_port1") + port2 = self.aedtapp.wave_port(assignment=self.output_face.name, name="test_port2") + + # Set different properties for each port + port1.specify_wave_direction = True + port2.specify_wave_direction = False + + port1.deembed = True + port2.deembed = False + + port1.renorm_all_modes = True + port2.renorm_all_modes = False + + # Verify properties are independent + assert port1.specify_wave_direction is True + assert port2.specify_wave_direction is False + + assert port1.deembed is True + assert port2.deembed is False + + assert port1.renorm_all_modes is True + assert port2.renorm_all_modes is False + + def test_19_impedance_renormalization_workflow(self): + """Test complete impedance renormalization workflow.""" + port = self.aedtapp.wave_port(assignment=self.input_face.name, name="test_port_workflow") + + # Test Impedance renormalization + port.renorm_impedance_type = "Impedance" + port.renorm_impedance = 75 + assert port.renorm_impedance == "75ohm" + + # Test RLC renormalization workflow + port.renorm_impedance_type = "RLC" + port.rlc_type = "Serial" + port.use_resistance = True + port.resistance_value = "50ohm" + port.use_inductance = True + port.inductance_value = "10nH" + port.use_capacitance = True + port.capacitance_value = "1pF" + + # Verify all settings were applied (no exceptions thrown) + assert port.renorm_impedance_type == "RLC" + + def test_20_integration_lines_complex_scenario(self): + """Test complex integration line scenarios.""" + port = self.aedtapp.wave_port( + assignment=self.input_face.name, + modes=4, + name="test_port_complex", + ) + + # Test alignment integration lines with all modes + integration_lines = [ + [[0, 0, 0], [0, 0, 1]], + [[0, 0, 0], [0, 1, 0]], + [[0, 0, 0], [0, 3, 0]], + [[0, 0, 0], [0, 5, 0]], + ] + alignment_groups = [1, 1, 2, 2] + + result = port.set_alignment_integration_line( + integration_lines, + coordinate_system="Global", + alignment_groups=alignment_groups, + ) + assert result is True + + # Switch to polarity mode + result = port.set_polarity_integration_line(integration_lines[:2]) + assert result is True + + # Verify mode alignment was disabled + # (This would need to be verified through properties access) + + # Test filter modes reporter with this port + port.filter_modes_reporter = [True, False, True, False] + expected = [True, False, True, False] + assert port.filter_modes_reporter == expected