diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index e0d97fcd9ef..ffc03217886 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -991,7 +991,7 @@ jobs: with: max_attempts: 2 retry_on: error - timeout_minutes: 20 + timeout_minutes: 30 command: | .venv\Scripts\Activate.ps1 pytest ${{ env.PYTEST_ARGUMENTS }} --timeout=600 -v -rA --color=yes -m emit diff --git a/doc/changelog.d/6768.fixed.md b/doc/changelog.d/6768.fixed.md new file mode 100644 index 00000000000..60ddb2585aa --- /dev/null +++ b/doc/changelog.d/6768.fixed.md @@ -0,0 +1 @@ +EMIT Pyaedt fixes diff --git a/src/ansys/aedt/core/emit_core/emit_constants.py b/src/ansys/aedt/core/emit_core/emit_constants.py index 5cfa5f62561..9d3a15098f2 100644 --- a/src/ansys/aedt/core/emit_core/emit_constants.py +++ b/src/ansys/aedt/core/emit_core/emit_constants.py @@ -168,20 +168,20 @@ def data_rate_conv(value: float, units: str, to_internal: bool = True): if units == "bps": mult = 1.0 elif units == "kbps": - mult = 1e-3 + mult = 1e3 elif units == "Mbps": - mult = 1e-6 + mult = 1e6 elif units == "Gbps": - mult = 1e-9 + mult = 1e9 else: if units == "bps": mult = 1.0 elif units == "kbps": - mult = 1e3 + mult = 1e-3 elif units == "Mbps": - mult = 1e6 + mult = 1e-6 elif units == "Gbps": - mult = 1e9 + mult = 1e-9 return value * mult diff --git a/src/ansys/aedt/core/emit_core/emit_schematic.py b/src/ansys/aedt/core/emit_core/emit_schematic.py index 2eb83555604..bb4b4b3a057 100644 --- a/src/ansys/aedt/core/emit_core/emit_schematic.py +++ b/src/ansys/aedt/core/emit_core/emit_schematic.py @@ -25,6 +25,7 @@ from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.internal.errors import AEDTRuntimeError class EmitSchematic: @@ -94,11 +95,19 @@ def create_component(self, component_type: str, name: str = None, library: str = try: # Retrieve matching components from the catalog + matching_components = [] matching_components = self.emit_instance.modeler.components.components_catalog[component_type] if not matching_components: - self.emit_instance.logger.error(f"No component found for type '{component_type}'.") - raise ValueError(f"No component found for type '{component_type}'.") + # couldn't find a component match, try looking at all component names + catalog_comps = self.emit_instance.modeler.components.components_catalog.components + for value in catalog_comps.values(): + if value.name == component_type: + matching_components.append(value) + + if not matching_components: + self.emit_instance.logger.error(f"No component found for type '{component_type}'.") + raise ValueError(f"No component found for type '{component_type}'.") if len(matching_components) == 1: # Use the single matching component @@ -123,6 +132,7 @@ def create_component(self, component_type: str, name: str = None, library: str = revision = self.emit_instance.results.get_revision() # Create the component using the EmitCom module + component.name = component.name.strip("'") new_component_id = self._emit_com_module.CreateEmitComponent( name, component.name, component.component_library ) @@ -203,3 +213,24 @@ def connect_components(self, component_name_1: str, component_name_2: str) -> No f"Failed to connect components '{component_name_1}' and '{component_name_2}': {e}" ) raise RuntimeError(f"Failed to connect components '{component_name_1}' and '{component_name_2}': {e}") + + @pyaedt_function_handler + def delete_component(self, name: str): + """Delete a component from the schematic. + + Parameters + ---------- + name : str + Name of the component. + + Raises + ------ + RuntimeError + If the deletion fails. + """ + try: + self._emit_com_module.DeleteEmitComponent(name) + self.emit_instance.logger.info(f"Successfully deleted component '{name}'.") + except Exception as e: + self.emit_instance.logger.error(f"Failed to delete component '{name}': {e}") + raise AEDTRuntimeError(f"Failed to delete component '{name}': {e}") diff --git a/src/ansys/aedt/core/emit_core/nodes/emit_node.py b/src/ansys/aedt/core/emit_core/nodes/emit_node.py index b9dc35146de..23ceb444695 100644 --- a/src/ansys/aedt/core/emit_core/nodes/emit_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/emit_node.py @@ -109,9 +109,12 @@ def _parent(self): Returns ------- EmitNode - Parent node name. + Parent node. """ - return self._get_property("Parent", True) + parent_name = self._get_property("Parent", True) + parent_name = parent_name.replace("NODE-*-", "") + node_id = self._oRevisionData.GetTopLevelNodeID(0, parent_name) + return self._get_node(node_id) @property def properties(self) -> dict: @@ -130,6 +133,22 @@ def properties(self) -> dict: def node_warnings(self) -> str: """Warnings for the node, if any. + .. deprecated: 0.21.3 + Use warnings property instead + + Returns + ------- + str + Warning message(s). + """ + warnings.warn("This property is deprecated in 0.21.3. Use the warnings property instead.", DeprecationWarning) + + return self.warnings + + @property + def warnings(self) -> str: + """Warnings for the node, if any. + Returns ------- str @@ -174,6 +193,7 @@ def _get_node(self, node_id: int): >>> new_node = node._get_node(node_id) """ from ansys.aedt.core.emit_core.nodes import generated + from ansys.aedt.core.emit_core.nodes.emitter_node import EmitterNode props = self._oRevisionData.GetEmitNodeProperties(self._result_id, node_id, True) props = self.props_to_dict(props) @@ -183,7 +203,23 @@ def _get_node(self, node_id: int): node = None try: - type_class = getattr(generated, f"{prefix}{node_type}") + type_class = EmitNode + if node_type == "RadioNode" and props["IsEmitter"] == "true": + type_class = EmitterNode + # TODO: enable when we add ReadOnlyNodes + # if prefix == "": + # type_class = EmitterNode + # else: + # type_class = ReadOnlyEmitterNode + elif node_type == "Band" and props["IsEmitterBand"] == "true": + type_class = getattr(generated, f"{prefix}Waveform") + elif node_type == "TxSpectralProfNode": + if self.properties["IsEmitterBand"] == "true": + type_class = getattr(generated, f"{prefix}TxSpectralProfEmitterNode") + else: + type_class = getattr(generated, f"{prefix}TxSpectralProfNode") + else: + type_class = getattr(generated, f"{prefix}{node_type}") node = type_class(self._emit_obj, self._result_id, node_id) except AttributeError: node = EmitNode(self._emit_obj, self._result_id, node_id) @@ -203,7 +239,7 @@ def children(self): child_nodes = [self._get_node(child_id) for child_id in child_ids] return child_nodes - def _get_property(self, prop, skipChecks=False) -> Union[str, List[str]]: + def _get_property(self, prop, skipChecks=False, isTable=False) -> Union[str, List[str]]: """Fetch the value of a given property. Parameters @@ -226,7 +262,14 @@ def _get_property(self, prop, skipChecks=False) -> Union[str, List[str]]: selected_kv_pair = selected_kv_pairs[0] val = selected_kv_pair[1] - if val.find("|") != -1: + if isTable: + # Node Prop tables + # Data formatted using compact string serialization + # with ';' separating rows and '|' separating columns + rows = val.split(";") + table = [tuple(row.split("|")) for row in rows if row] + return table + elif val.find("|") != -1: return val.split("|") else: return val @@ -271,6 +314,7 @@ def _string_to_value_units(value) -> tuple[float, str]: # see if we can split it based on a space between number # and units vals = value.split(" ") + units = "" if len(vals) == 2: dec_val = float(vals[0]) units = vals[1].strip() @@ -281,7 +325,11 @@ def _string_to_value_units(value) -> tuple[float, str]: dec_val = float(value[:i]) units = value[i:] return dec_val, units - raise ValueError(f"{value} is not valid for this property.") + # maybe it's a string but with no units + try: + return float(value), units + except ValueError: + raise ValueError(f"{value} is not valid for this property.") def _convert_to_internal_units(self, value: float | str, unit_system: str) -> float: """Takes a value and converts to internal EMIT units used for storing values. @@ -301,9 +349,20 @@ def _convert_to_internal_units(self, value: float | str, unit_system: str) -> fl """ if isinstance(value, float) or isinstance(value, int): # unitless, so assume SI Units - units = consts.SI_UNITS[unit_system] + if unit_system == "Data Rate": + # Data rate isn't included as part of PyAedt's unit class + units = "bps" + else: + units = consts.SI_UNITS[unit_system] else: value, units = self._string_to_value_units(value) + # make sure units were specified, if not use SI Units + if units == "": + if unit_system == "Data Rate": + # Data rate isn't included as part of PyAedt unit class + units = "bps" + else: + units = consts.SI_UNITS[unit_system] # verify the units are valid for the specified type if units not in EMIT_VALID_UNITS[unit_system]: raise ValueError(f"{units} are not valid units for this property.") @@ -331,10 +390,11 @@ def _convert_from_internal_units(value: float, unit_system: str) -> float: Value in SI units. """ # get the SI units - units = consts.SI_UNITS[unit_system] if unit_system == "Data Rate": + units = "bps" converted_value = data_rate_conv(value, units, False) else: + units = consts.SI_UNITS[unit_system] converted_value = consts.unit_converter(value, unit_system, EMIT_INTERNAL_UNITS[unit_system], units) return converted_value @@ -424,28 +484,88 @@ def _get_child_node_id(self, child_name: str) -> int: """ return self._oRevisionData.GetChildNodeID(self._result_id, self._node_id, child_name) + def _is_column_data_table(self): + """Returns true if the node uses column data tables. + + Returns + ------- + bool + True if the table is ColumnData, False otherwise. + """ + # BB Emission Nodes can have ColumnData or NodeProp tables + # so handle them first + if self._node_type == "TxBbEmissionNode": + if self._get_property("Noise Behavior") == "BroadbandEquation": + return False + return True + + table_title = self._get_property("CDTableTitle", True) + if table_title == "": + # No ColumnData Table Title, so it's a NodePropTable + return False + return True + def _get_table_data(self): """Returns the node's table data. Returns ------- - list - The node's table data. + list of tuples + The node's table data as a list of tuples. + [(x1, y1, z1), (x2, y2, z2)] """ - rows = self._oRevisionData.GetTableData(self._result_id, self._node_id) - nested_list = [col.split(" ") for col in rows] - return nested_list + try: + if self._is_column_data_table(): + # Column Data tables + # Data formatted using compact string serialization + # with '|' separating rows and ';' separating columns + data = self._oRevisionData.GetTableData(self._result_id, self._node_id) + rows = data.split("|") + string_table = [tuple(row.split(";")) for row in rows if row] + table = [tuple(float(x) for x in t) for t in string_table] + else: + # Node Prop tables + # Data formatted using compact string serialization + # with ';' separating rows and '|' separating columns + table_key = self._get_property("TableKey", True) + string_table = self._get_property(table_key, True, True) + + def try_float(val): + try: + return float(val) + except ValueError: + return val # keep as string for non-numeric (e.g. equations) + + table = [tuple(try_float(x) for x in t) for t in string_table] + except Exception as e: + print(f"Failed to get table data for node {self.name}. Error: {e}") + return table - def _set_table_data(self, nested_list): + def _set_table_data(self, table): """Sets the table data for the node. Parameters ---------- - nested_list : list + list of tuples Data to populate the table with. + [(x1, y1, z1), (x2, y2, z2)] """ - rows = [col.join(" ") for col in nested_list] - self._oRevisionData.SetTableData(self._result_id, self._node_id, rows) + try: + if self._is_column_data_table(): + # Column Data tables + # Data formatted using compact string serialization + # with '|' separating rows and ';' separating columns + data = "|".join(";".join(map(str, row)) for row in table) + self._oRevisionData.SetTableData(self._result_id, self._node_id, data) + else: + # Node Prop tables + # Data formatted using compact string serialization + # with ';' separating rows and '|' separating columns + table_key = self._get_property("TableKey", True) + data = ";".join("|".join(map(str, row)) for row in table) + self._set_property(table_key, data) + except Exception as e: + print(f"Failed to set table data for node {self.name}. Error: {e}") def _add_child_node(self, child_type, child_name=None): """Creates a child node of the given type and name. @@ -455,12 +575,12 @@ def _add_child_node(self, child_type, child_name=None): child_type : EmitNode Type of child node to create. child_name : str, optional - Optional name to use for the child node. If None, a default name is used. + Name to use for the child node. If None, a default name is used. Returns ------- - int - Unique node ID assigned to the created child node. + node: EmitNode + The node. Raises ------ @@ -468,15 +588,16 @@ def _add_child_node(self, child_type, child_name=None): If the specified child type is not allowed. """ if not child_name: - child_name = f"New {child_type}" + child_name = f"{child_type}" - new_id = None + new_node = None if child_type not in self.allowed_child_types: raise ValueError( f"Child type {child_type} is not allowed for this node. Allowed types are: {self.allowed_child_types}" ) try: new_id = self._oRevisionData.CreateEmitNode(self._result_id, self._node_id, child_name, child_type) + new_node = self._get_node(new_id) except Exception as e: print(f"Failed to add child node of type {child_type} to node {self.name}. Error: {e}") - return new_id + return new_node diff --git a/src/ansys/aedt/core/emit_core/nodes/emitter_node.py b/src/ansys/aedt/core/emit_core/nodes/emitter_node.py index 28807963674..f6d8c6cd80d 100644 --- a/src/ansys/aedt/core/emit_core/nodes/emitter_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/emitter_node.py @@ -26,6 +26,7 @@ from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode from ansys.aedt.core.emit_core.nodes.generated import AntennaNode +from ansys.aedt.core.emit_core.nodes.generated import BandFolder from ansys.aedt.core.emit_core.nodes.generated import RadioNode from ansys.aedt.core.emit_core.nodes.generated import Waveform @@ -65,6 +66,18 @@ def __init__(self, emit_obj, result_id, node_id): ant_id = self._oRevisionData.GetChildNodeID(result_id, scene_node_id, ant) self._antenna_node = AntennaNode(emit_obj, result_id, ant_id) + def node_type(self) -> str: + """The type of this emit node""" + return "EmitterNode" + + def duplicate(self, new_name: str): + """Duplicate this node""" + return self._duplicate(new_name) + + def delete(self): + """Delete this node""" + self._delete() + def get_radio(self) -> RadioNode: """Get the radio associated with this Emitter. @@ -93,6 +106,20 @@ def get_antenna(self) -> AntennaNode: """ return self._antenna_node + def children(self): + """Overridden to return the Waveforms + + Returns + ------- + waveforms: list[Waveform] + list of waveform nodes defined for the Emitter. + + Examples + -------- + >>> waveforms = emitter.get_waveforms() + """ + return self.get_waveforms() + def get_waveforms(self) -> list[Waveform]: """Get the waveform nodes for the Emitter. @@ -110,12 +137,12 @@ def get_waveforms(self) -> list[Waveform]: waveforms = [] # check for folders and recurse them if needed for child in radio_children: - if child.type == "BandFolder": + if isinstance(child, BandFolder): grandchildren = child.children for grandchild in grandchildren: - # don't allow nested folders, so can add these + # we don't allow nested folders, so can add these # directly to the waveform list waveforms.append(cast(Waveform, grandchild)) - elif child.type == "Band": + elif isinstance(child, Waveform): waveforms.append(cast(Waveform, child)) return waveforms diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/__init__.py b/src/ansys/aedt/core/emit_core/nodes/generated/__init__.py index 2820de648f5..190fd9de11c 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/__init__.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/__init__.py @@ -30,7 +30,6 @@ from .band_folder import BandFolder from .cable import Cable from .cad_node import CADNode -from .categories_view_node import CategoriesViewNode from .circulator import Circulator from .coupling_link_node import CouplingLinkNode from .couplings_node import CouplingsNode @@ -61,7 +60,6 @@ from .solution_coupling_node import SolutionCouplingNode from .solutions_node import SolutionsNode from .terminator import Terminator -from .top_level_simulation import TopLevelSimulation from .touchstone_coupling_node import TouchstoneCouplingNode from .tr_switch import TR_Switch from .two_ray_path_loss_coupling_node import TwoRayPathLossCouplingNode @@ -83,7 +81,6 @@ "BandFolder", "CADNode", "Cable", - "CategoriesViewNode", "Circulator", "CouplingLinkNode", "CouplingsNode", @@ -115,7 +112,6 @@ "SolutionsNode", "TR_Switch", "Terminator", - "TopLevelSimulation", "TouchstoneCouplingNode", "TwoRayPathLossCouplingNode", "TxBbEmissionNode", diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/amplifier.py b/src/ansys/aedt/core/emit_core/nodes/generated/amplifier.py index c556de6ea93..8ce07129130 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/amplifier.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/amplifier.py @@ -23,7 +23,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from enum import Enum from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode @@ -50,6 +49,21 @@ def delete(self): """Delete this node""" self._delete() + @property + def table_data(self): + """Harmonic Intercept Points, Ref. Input Table. + Table consists of 2 columns. + Harmonic: + Value should be between 2 and 20. + Intercept Point: + Value should be between -1000 and 1000. + """ + return self._get_table_data() + + @table_data.setter + def table_data(self, value): + self._set_table_data(value) + @property def filename(self) -> str: """Name of file defining the outboard component. @@ -86,21 +100,6 @@ def notes(self) -> str: def notes(self, value: str): self._set_property("Notes", f"{value}") - class AmplifierTypeOption(Enum): - TRANSMIT_AMPLIFIER = "Transmit Amplifier" - RECEIVE_AMPLIFIER = "Receive Amplifier" - - @property - def amplifier_type(self) -> AmplifierTypeOption: - """Configures the amplifier as a Tx or Rx amplifier.""" - val = self._get_property("Amplifier Type") - val = self.AmplifierTypeOption[val.upper()] - return val - - @amplifier_type.setter - def amplifier_type(self, value: AmplifierTypeOption): - self._set_property("Amplifier Type", f"{value.value}") - @property def gain(self) -> float: """Amplifier in-band gain. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/band.py b/src/ansys/aedt/core/emit_core/nodes/generated/band.py index 057ea7d77cb..ffa5753959f 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/band.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/band.py @@ -52,16 +52,6 @@ def enabled(self) -> bool: def enabled(self, value: bool): self._set_property("Enabled", f"{str(value).lower()}") - @property - def port(self): - """Radio Port associated with this Band.""" - val = self._get_property("Port") - return val - - @port.setter - def port(self, value): - self._set_property("Port", f"{value}") - @property def use_dd_1494_mode(self) -> bool: """Uses DD-1494 parameters to define the Tx/Rx spectrum. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/categories_view_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/categories_view_node.py deleted file mode 100644 index 07e43c5a2a7..00000000000 --- a/src/ansys/aedt/core/emit_core/nodes/generated/categories_view_node.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-FileCopyrightText: 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. - -from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode - - -class CategoriesViewNode(EmitNode): - def __init__(self, emit_obj, result_id, node_id): - self._is_component = False - EmitNode.__init__(self, emit_obj, result_id, node_id) - - @property - def node_type(self) -> str: - """The type of this emit node.""" - return self._node_type diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/circulator.py b/src/ansys/aedt/core/emit_core/nodes/generated/circulator.py index 3475dd5b427..551ef188e5c 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/circulator.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/circulator.py @@ -105,21 +105,6 @@ def type(self) -> TypeOption: def type(self, value: TypeOption): self._set_property("Type", f"{value.value}") - class Port1LocationOption(Enum): - RADIO_SIDE = "Radio Side" - ANTENNA_SIDE = "Antenna Side" - - @property - def port_1_location(self) -> Port1LocationOption: - """Defines the orientation of the circulator.""" - val = self._get_property("Port 1 Location") - val = self.Port1LocationOption[val.upper()] - return val - - @port_1_location.setter - def port_1_location(self, value: Port1LocationOption): - self._set_property("Port 1 Location", f"{value.value}") - @property def insertion_loss(self) -> float: """Circulator in-band loss in forward direction. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/custom_coupling_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/custom_coupling_node.py index 3d6e5b12b0e..ece0968a02d 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/custom_coupling_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/custom_coupling_node.py @@ -59,7 +59,7 @@ def delete(self): @property def table_data(self): - """Table. + """Custom Coupling Values Table. Table consists of 2 columns. Frequency: Value should be between 1.0 and 100.0e9. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/indoor_propagation_coupling_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/indoor_propagation_coupling_node.py index f6c7fd0230f..2075210b97a 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/indoor_propagation_coupling_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/indoor_propagation_coupling_node.py @@ -57,7 +57,7 @@ def delete(self): @property def table_data(self): - """Table. + """Custom Building Values Table. Table consists of 3 columns. Frequency: Value should be between 1.0 and 100.0e9. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/isolator.py b/src/ansys/aedt/core/emit_core/nodes/generated/isolator.py index 4e3ee8d286b..8bebfe07784 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/isolator.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/isolator.py @@ -105,21 +105,6 @@ def type(self) -> TypeOption: def type(self, value: TypeOption): self._set_property("Type", f"{value.value}") - class Port1LocationOption(Enum): - RADIO_SIDE = "Radio Side" - ANTENNA_SIDE = "Antenna Side" - - @property - def port_1_location(self) -> Port1LocationOption: - """Defines the orientation of the isolator.""" - val = self._get_property("Port 1 Location") - val = self.Port1LocationOption[val.upper()] - return val - - @port_1_location.setter - def port_1_location(self, value: Port1LocationOption): - self._set_property("Port 1 Location", f"{value.value}") - @property def insertion_loss(self) -> float: """Isolator in-band loss in forward direction. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/multiplexer.py b/src/ansys/aedt/core/emit_core/nodes/generated/multiplexer.py index c1b9a6fa535..6225e44da75 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/multiplexer.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/multiplexer.py @@ -110,34 +110,6 @@ def type(self) -> TypeOption: def type(self, value: TypeOption): self._set_property("Type", f"{value.value}") - class Port1LocationOption(Enum): - RADIO_SIDE = "Radio Side" - ANTENNA_SIDE = "Antenna Side" - - @property - def port_1_location(self) -> Port1LocationOption: - """Defines the orientation of the multiplexer.""" - val = self._get_property("Port 1 Location") - val = self.Port1LocationOption[val.upper()] - return val - - @port_1_location.setter - def port_1_location(self, value: Port1LocationOption): - self._set_property("Port 1 Location", f"{value.value}") - - @property - def flip_ports_vertically(self) -> bool: - """Reverses the port order on the multi-port side of the multiplexer. - - Value should be 'true' or 'false'. - """ - val = self._get_property("Flip Ports Vertically") - return val == "true" - - @flip_ports_vertically.setter - def flip_ports_vertically(self, value: bool): - self._set_property("Flip Ports Vertically", f"{str(value).lower()}") - @property def ports(self): """Assigns the child port nodes to the multiplexers ports.""" diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/power_divider.py b/src/ansys/aedt/core/emit_core/nodes/generated/power_divider.py index 57b5c8ca6df..db5d90afb57 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/power_divider.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/power_divider.py @@ -106,21 +106,6 @@ def type(self) -> TypeOption: def type(self, value: TypeOption): self._set_property("Type", f"{value.value}") - class OrientationOption(Enum): - DIVIDER = "Divider" - COMBINER = "Combiner" - - @property - def orientation(self) -> OrientationOption: - """Defines the orientation of the Power Divider.""" - val = self._get_property("Orientation") - val = self.OrientationOption[val.upper()] - return val - - @orientation.setter - def orientation(self, value: OrientationOption): - self._set_property("Orientation", f"{value.value}") - @property def insertion_loss_above_ideal(self) -> float: """Insertion Loss Above Ideal. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/rx_mixer_product_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/rx_mixer_product_node.py index c897e0b496f..73ac87b3bd7 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/rx_mixer_product_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/rx_mixer_product_node.py @@ -51,6 +51,23 @@ def delete(self): """Delete this node""" self._delete() + @property + def table_data(self): + """Edit Mixer Products Table. + Table consists of 3 columns. + RF Harmonic Order: + Value should be between -100 and 100. + LO Harmonic Order: + Value should be between 1 and 100. + Power (Relative or Absolute): + Value should be between -1000 and 1000. + """ + return self._get_table_data() + + @table_data.setter + def table_data(self, value): + self._set_table_data(value) + @property def enabled(self) -> bool: """Enabled state for this node.""" diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/rx_saturation_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/rx_saturation_node.py index 03d65ecb7d7..a84a5965a7d 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/rx_saturation_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/rx_saturation_node.py @@ -49,6 +49,21 @@ def delete(self): """Delete this node""" self._delete() + @property + def table_data(self): + """Rx Saturation Profile Table. + Table consists of 2 columns. + Frequency: + Value should be between 1 and 100e9. + Amplitude: + Value should be between -1000 and 1000. + """ + return self._get_table_data() + + @table_data.setter + def table_data(self, value): + self._set_table_data(value) + @property def enabled(self) -> bool: """Enabled state for this node.""" diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/rx_selectivity_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/rx_selectivity_node.py index 9eb7a00f97b..9c8ea1d3380 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/rx_selectivity_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/rx_selectivity_node.py @@ -49,6 +49,21 @@ def delete(self): """Delete this node""" self._delete() + @property + def table_data(self): + """Rx Selectivity Table. + Table consists of 2 columns. + Bandwidth: + Value should be between 0 and 100e9. + Attenuation: + Value should be between -200 and 1000. + """ + return self._get_table_data() + + @table_data.setter + def table_data(self, value): + self._set_table_data(value) + @property def enabled(self) -> bool: """Enabled state for this node.""" diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/rx_spur_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/rx_spur_node.py index ab69c821415..2c14267787d 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/rx_spur_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/rx_spur_node.py @@ -53,7 +53,7 @@ def delete(self): @property def table_data(self): - """Table. + """Spurs Table. Table consists of 3 columns. Frequency (MHz): Value should be a mathematical expression. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/sampling_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/sampling_node.py index 5b10df45486..701de81f7bd 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/sampling_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/sampling_node.py @@ -45,12 +45,12 @@ def node_type(self) -> str: @property def table_data(self): - """Table. + """Frequency Ranges Table. Table consists of 2 columns. Min: - Value should be greater than 1.0. + Value should be between 1.0 and 100e9. Max: - Value should be greater than 1.0. + Value should be between 1.0 and 100e9. """ return self._get_table_data() diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/terminator.py b/src/ansys/aedt/core/emit_core/nodes/generated/terminator.py index c7e7a51fce6..2e37e3f2ae8 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/terminator.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/terminator.py @@ -50,6 +50,23 @@ def delete(self): """Delete this node""" self._delete() + @property + def table_data(self): + """Parametric VSWR Table. + Table consists of 3 columns. + Min: + Value should be between 1 and 100e9. + Max: + Value should be between 1 and 100e9. + VSWR: + + """ + return self._get_table_data() + + @table_data.setter + def table_data(self, value): + self._set_table_data(value) + @property def filename(self) -> str: """Name of file defining the Terminator. @@ -105,21 +122,6 @@ def type(self) -> TypeOption: def type(self, value: TypeOption): self._set_property("Type", f"{value.value}") - class PortLocationOption(Enum): - RADIO_SIDE = "Radio Side" - ANTENNA_SIDE = "Antenna Side" - - @property - def port_location(self) -> PortLocationOption: - """Defines the orientation of the terminator.""" - val = self._get_property("Port Location") - val = self.PortLocationOption[val.upper()] - return val - - @port_location.setter - def port_location(self, value: PortLocationOption): - self._set_property("Port Location", f"{value.value}") - @property def vswr(self) -> float: """VSWR. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/top_level_simulation.py b/src/ansys/aedt/core/emit_core/nodes/generated/top_level_simulation.py deleted file mode 100644 index 9f64dfaa93e..00000000000 --- a/src/ansys/aedt/core/emit_core/nodes/generated/top_level_simulation.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-FileCopyrightText: 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. - -from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode - - -class TopLevelSimulation(EmitNode): - def __init__(self, emit_obj, result_id, node_id): - self._is_component = False - EmitNode.__init__(self, emit_obj, result_id, node_id) - - @property - def node_type(self) -> str: - """The type of this emit node.""" - return self._node_type diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/tr_switch.py b/src/ansys/aedt/core/emit_core/nodes/generated/tr_switch.py index c73d860719b..70fed7d32cc 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/tr_switch.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/tr_switch.py @@ -23,7 +23,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from enum import Enum from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode @@ -86,36 +85,6 @@ def notes(self) -> str: def notes(self, value: str): self._set_property("Notes", f"{value}") - class TxPortOption(Enum): - PORT_1 = "Port 1" - PORT_2 = "Port 2" - - @property - def tx_port(self) -> TxPortOption: - """Specifies which port on the TR Switch is part of the Tx path.""" - val = self._get_property("Tx Port") - val = self.TxPortOption[val.upper()] - return val - - @tx_port.setter - def tx_port(self, value: TxPortOption): - self._set_property("Tx Port", f"{value.value}") - - class CommonPortLocationOption(Enum): - RADIO_SIDE = "Radio Side" - ANTENNA_SIDE = "Antenna Side" - - @property - def common_port_location(self) -> CommonPortLocationOption: - """Defines the orientation of the tr switch.""" - val = self._get_property("Common Port Location") - val = self.CommonPortLocationOption[val.upper()] - return val - - @common_port_location.setter - def common_port_location(self, value: CommonPortLocationOption): - self._set_property("Common Port Location", f"{value.value}") - @property def insertion_loss(self) -> float: """TR Switch in-band loss in forward direction. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/tx_bb_emission_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/tx_bb_emission_node.py index a0144b4e998..3db2f83ed5c 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/tx_bb_emission_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/tx_bb_emission_node.py @@ -55,10 +55,10 @@ def delete(self): def table_data(self): """Tx Broadband Noise Profile Table. Table consists of 2 columns. - Frequency (MHz): - Value should be a mathematical expression. - Amplitude (dBm/Hz): - Value should be between -200.0 and 150.0. + Frequency, Bandwidth, or Offset: + Value should be between -100e9 and 100e9. + Amplitude: + Value should be between -1000 and 200. """ return self._get_table_data() diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/tx_harmonic_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/tx_harmonic_node.py index 8fb5754bc61..d5b24df5a79 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/tx_harmonic_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/tx_harmonic_node.py @@ -51,6 +51,21 @@ def delete(self): """Delete this node""" self._delete() + @property + def table_data(self): + """Edit Harmonics Table. + Table consists of 2 columns. + Harmonic: + Value should be between 2 and 1000. + Power (Relative or Absolute): + Value should be between -1000 and 1000. + """ + return self._get_table_data() + + @table_data.setter + def table_data(self, value): + self._set_table_data(value) + @property def enabled(self) -> bool: """Enabled state for this node.""" diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/tx_nb_emission_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/tx_nb_emission_node.py index 3f0a9c551f9..3888c2f9ec6 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/tx_nb_emission_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/tx_nb_emission_node.py @@ -51,6 +51,21 @@ def delete(self): """Delete this node""" self._delete() + @property + def table_data(self): + """Tx Emissions Profile Table. + Table consists of 2 columns. + Bandwidth or Frequency: + Value should be between 1 and 100e9. + Attenuation or Power: + Value should be between -1000 and 1000. + """ + return self._get_table_data() + + @table_data.setter + def table_data(self, value): + self._set_table_data(value) + @property def enabled(self) -> bool: """Enabled state for this node.""" diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/tx_spur_node.py b/src/ansys/aedt/core/emit_core/nodes/generated/tx_spur_node.py index 23857e1e35e..a40a55ea351 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/tx_spur_node.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/tx_spur_node.py @@ -53,7 +53,7 @@ def delete(self): @property def table_data(self): - """Table. + """Spurs Table. Table consists of 3 columns. Frequency (MHz): Value should be a mathematical expression. diff --git a/src/ansys/aedt/core/emit_core/nodes/generated/waveform.py b/src/ansys/aedt/core/emit_core/nodes/generated/waveform.py index 38e8a392881..b9e3354c6a5 100644 --- a/src/ansys/aedt/core/emit_core/nodes/generated/waveform.py +++ b/src/ansys/aedt/core/emit_core/nodes/generated/waveform.py @@ -52,16 +52,6 @@ def enabled(self) -> bool: def enabled(self, value: bool): self._set_property("Enabled", f"{str(value).lower()}") - @property - def port(self): - """Radio Port associated with this Band.""" - val = self._get_property("Port") - return val - - @port.setter - def port(self, value): - self._set_property("Port", f"{value}") - class WaveformOption(Enum): PERIODIC_CLOCK = "Periodic Clock" SPREAD_SPECTRUM_CLOCK = "Spread Spectrum Clock" diff --git a/src/ansys/aedt/core/emit_core/results/revision.py b/src/ansys/aedt/core/emit_core/results/revision.py index 2d0830caf57..9f59b5c8a55 100644 --- a/src/ansys/aedt/core/emit_core/results/revision.py +++ b/src/ansys/aedt/core/emit_core/results/revision.py @@ -30,9 +30,11 @@ from ansys.aedt.core.emit_core.emit_constants import TxRxMode from ansys.aedt.core.emit_core.nodes import generated from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode +from ansys.aedt.core.emit_core.nodes.emitter_node import EmitterNode from ansys.aedt.core.emit_core.nodes.generated import CouplingsNode from ansys.aedt.core.emit_core.nodes.generated import EmitSceneNode from ansys.aedt.core.emit_core.nodes.generated import ResultPlotNode +from ansys.aedt.core.emit_core.nodes.generated import Waveform from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.internal.checks import min_aedt_version @@ -1013,12 +1015,26 @@ def _get_node(self, node_id: int) -> EmitNode: node = None try: + type_class = EmitNode if node_type == "RadioNode" and props["IsEmitter"] == "true": - type_class = getattr(generated, f"{prefix}EmitterNode") - node = type_class(self.emit_project, self.results_index, node_id) + type_class = EmitterNode + # TODO: enable when we add ReadOnlyNodes + # if prefix == "": + # type_class = EmitterNode + # else: + # type_class = ReadOnlyEmitterNode + elif node_type == "Band" and props["IsEmitterBand"] == "true": + type_class = getattr(generated, f"{prefix}Waveform") + elif node_type == "TxSpectralProfNode": + parent_name = props["Parent"] + parent_name = parent_name.replace("NODE-*-", "") + node_id = self._emit_com.GetTopLevelNodeID(0, parent_name) + parent_node = self._get_node(node_id) + if isinstance(parent_node, Waveform): + type_class = getattr(generated, f"{prefix}TxSpectralProfEmitterNode") else: type_class = getattr(generated, f"{prefix}{node_type}") - node = type_class(self.emit_project, self.results_index, node_id) + node = type_class(self.emit_project, self.results_index, node_id) except AttributeError: node = EmitNode(self.emit_project, self.results_index, node_id) return node diff --git a/src/ansys/aedt/core/generic/constants.py b/src/ansys/aedt/core/generic/constants.py index b0bbcf55787..72f0cba0942 100644 --- a/src/ansys/aedt/core/generic/constants.py +++ b/src/ansys/aedt/core/generic/constants.py @@ -351,7 +351,8 @@ def validate_enum_class_value(cls, value): "mil": METER2IN * 1e-3, "in": METER2IN, "ft": METER2IN * 12, - "yd": METER2IN * 144, + "yd": METER2IN * 36, + "mile": METER2IN * 63360, }, "Mass": {"ug": 1e-9, "mg": 1e-6, "g": 1e-3, "kg": 1.0, "ton": 1000, "oz": 0.0283495, "lb": 0.453592}, "None": { diff --git a/tests/system/emit/test_emit.py b/tests/system/emit/test_emit.py index f789bfb6af0..6de9f343ca2 100644 --- a/tests/system/emit/test_emit.py +++ b/tests/system/emit/test_emit.py @@ -53,10 +53,26 @@ from ansys.aedt.core.emit_core.emit_constants import TxRxMode from ansys.aedt.core.emit_core.nodes import generated from ansys.aedt.core.emit_core.nodes.emit_node import EmitNode + from ansys.aedt.core.emit_core.nodes.emitter_node import EmitterNode + from ansys.aedt.core.emit_core.nodes.generated import Amplifier + from ansys.aedt.core.emit_core.nodes.generated import AntennaNode from ansys.aedt.core.emit_core.nodes.generated import Band from ansys.aedt.core.emit_core.nodes.generated import Filter from ansys.aedt.core.emit_core.nodes.generated import RadioNode + from ansys.aedt.core.emit_core.nodes.generated import RxMixerProductNode + from ansys.aedt.core.emit_core.nodes.generated import RxSaturationNode + from ansys.aedt.core.emit_core.nodes.generated import RxSelectivityNode + from ansys.aedt.core.emit_core.nodes.generated import RxSpurNode + from ansys.aedt.core.emit_core.nodes.generated import RxSusceptibilityProfNode from ansys.aedt.core.emit_core.nodes.generated import SamplingNode + from ansys.aedt.core.emit_core.nodes.generated import Terminator + from ansys.aedt.core.emit_core.nodes.generated import TxBbEmissionNode + from ansys.aedt.core.emit_core.nodes.generated import TxHarmonicNode + from ansys.aedt.core.emit_core.nodes.generated import TxNbEmissionNode + from ansys.aedt.core.emit_core.nodes.generated import TxSpectralProfEmitterNode + from ansys.aedt.core.emit_core.nodes.generated import TxSpectralProfNode + from ansys.aedt.core.emit_core.nodes.generated import TxSpurNode + from ansys.aedt.core.emit_core.nodes.generated import Waveform from ansys.aedt.core.modeler.circuits.primitives_emit import EmitAntennaComponent from ansys.aedt.core.modeler.circuits.primitives_emit import EmitComponent from ansys.aedt.core.modeler.circuits.primitives_emit import EmitComponents @@ -586,10 +602,8 @@ def get_sampling_node(rad_name): sampling = get_sampling_node(rad3.name) mode_rx = TxRxMode.RX - assert ( - sampling.parent + "-*-" + sampling.name - == "NODE-*-RF Systems-*-Bluetooth 2-*-Radios-*-Bluetooth 2-*-Sampling" - ) + assert sampling.parent.name + "-*-" + sampling.name == "Bluetooth 2-*-Sampling" + sampling.specify_percentage = True sampling.percentage_of_channels = 25 rev = emit_app.results.analyze() @@ -1800,10 +1814,280 @@ def test_fm_fsk_freq_deviation(self, emit_app): band_node.freq_deviation = 1e4 assert band_node.freq_deviation == 1e4 - @pytest.mark.skipif(config["desktopVersion"] < "2025.1", reason="Skipped on versions earlier than 2024 R2.") - @pytest.mark.skipif(config["desktopVersion"] <= "2026.1", reason="Not stable test") + @pytest.mark.skipif(config["desktopVersion"] < "2025.2", reason="Skipped on versions earlier than 2025 R2.") + def test_tables(self, emit_app): + # Emit has 2 different types of tables: Node Prop Tables and ColumnData Tables + # this test confirms that the table_data properties work for both + radio = emit_app.schematic.create_component("New Radio") + radio = cast(RadioNode, radio) + + children = radio.children + for child in children: + if child.node_type == "SamplingNode": + sampling = cast(SamplingNode, child) + + # Sampling node's use NodeProp tables + # Verify the table is empty by default + assert sampling.table_data == [] + + # Set the sampling table + sampling_data = [(100000000.0, 100000000.0), (200000000.0, 200000000.0)] + sampling.table_data = sampling_data + + # Get the sampling table and verify the data was set properly + assert sampling.table_data == sampling_data + + # Now add an amplifier and set its table to test ColumnData Tables + amp = emit_app.schematic.create_component("Amplifier") + amp = cast(Amplifier, amp) + + # Verify the table is empty by default + assert amp.table_data == [] + + # Set the amplifier table + amp_data = [(2, -50.0), (3, -60.0)] + amp.table_data = amp_data + + # Get the amplifier table and verify the data was set properly + assert amp.table_data == amp_data + + if config["desktopVersion"] >= "2026.1": + # Test BB Emissions Node since it can be either a NodeProp or + # ColumnData Table + radio2 = emit_app.schematic.create_component("New Radio") + radio2 = cast(RadioNode, radio2) + + children = radio2.children + tx_spec = None + for child in children: + if child.node_type == "Band": + band_children = child.children + for band_child in band_children: + if band_child.node_type == "TxSpectralProfNode": + tx_spec = cast(TxSpectralProfNode, band_child) + + bb_noise = tx_spec.add_tx_broadband_noise_profile() + bb_noise = cast(TxBbEmissionNode, bb_noise) + + # verify the table is empty by default + assert bb_noise.table_data == [] + + # Set the ColumnData Table + bb_data = [(100000.0, -170.0), (100000000.0, -160.0), (200000000.0, -170.0)] + bb_noise.table_data = bb_data + + # Verify the ColumnData Table was set + assert bb_noise.table_data == bb_data + + # Change it to a NodeProp Table (Equation based) + bb_data = [("RF+10", -160), ("RF+100", -166)] + bb_noise.noise_behavior = TxBbEmissionNode.NoiseBehaviorOption.EQUATION + bb_noise.table_data = bb_data + + # Verify the NodeProp Table was set + assert bb_noise.table_data == bb_data + + @pytest.mark.skipif(config["desktopVersion"] < "2025.2", reason="Skipped on versions earlier than 2025 R2.") + def test_emitters_radios(self, emit_app): + emitter_name = "Test Emitter" + emitter_node: EmitterNode = emit_app.schematic.create_component( + name=emitter_name, component_type="New Emitter", library="Emitters" + ) + + # Test that you can get the emitter's radio and antenna nodes + emitter_radio: RadioNode = emitter_node.get_radio() + assert isinstance(emitter_radio, RadioNode) + + emitter_ant: AntennaNode = emitter_node.get_antenna() + assert isinstance(emitter_ant, AntennaNode) + + emitter_band: Waveform = emitter_node.get_waveforms()[0] + assert emitter_band.warnings == "" + + assert emitter_node.children() == emitter_node.get_waveforms() + + emitter_band.waveform = Waveform.WaveformOption.PRBS + assert emitter_band.waveform == Waveform.WaveformOption.PRBS + + tx_spec: TxSpectralProfEmitterNode = emitter_band.children[0] + assert isinstance(tx_spec, TxSpectralProfEmitterNode) + + radio_node: RadioNode = emit_app.schematic.create_component("New Radio", "Radios") + + band: Band = radio_node.children[0] + assert isinstance(band, Band) + + radio_tx_spec: TxSpectralProfNode = band.children[0] + assert isinstance(radio_tx_spec, TxSpectralProfNode) + + radio_rx_spec: RxSusceptibilityProfNode = band.children[1] + assert isinstance(radio_rx_spec, RxSusceptibilityProfNode) + + # test the Tx Spectral Profile child nodes + radio_harmonics: TxHarmonicNode = radio_tx_spec.add_custom_tx_harmonics() + assert isinstance(radio_harmonics, TxHarmonicNode) + assert len(radio_harmonics.table_data) == 0 + + radio_bb_noise: TxBbEmissionNode = radio_tx_spec.add_tx_broadband_noise_profile() + assert isinstance(radio_bb_noise, TxBbEmissionNode) + assert len(radio_bb_noise.table_data) == 0 + + radio_nb_emissions: TxNbEmissionNode = radio_tx_spec.add_narrowband_emissions_mask() + assert isinstance(radio_nb_emissions, TxNbEmissionNode) + assert len(radio_nb_emissions.table_data) == 0 + + radio_tx_spur: TxSpurNode = radio_tx_spec.add_spurious_emissions() + assert isinstance(radio_tx_spur, TxSpurNode) + assert len(radio_tx_spur.table_data) == 0 + + # test the Rx Spectral Profile child nodes + radio_saturation: RxSaturationNode = radio_rx_spec.add_rx_saturation() + assert isinstance(radio_saturation, RxSaturationNode) + assert len(radio_saturation.table_data) == 0 + + radio_selectivity: RxSelectivityNode = radio_rx_spec.add_rx_selectivity() + assert isinstance(radio_selectivity, RxSelectivityNode) + assert len(radio_selectivity.table_data) == 0 + + radio_mixer_products: RxMixerProductNode = radio_rx_spec.add_mixer_products() + assert isinstance(radio_mixer_products, RxMixerProductNode) + assert len(radio_mixer_products.table_data) == 0 + + radio_rx_spurs: RxSpurNode = radio_rx_spec.add_spurious_responses() + assert isinstance(radio_rx_spurs, RxSpurNode) + assert len(radio_rx_spurs.table_data) == 0 + + # Test deleting components + emit_app.schematic.delete_component(radio_node.name) + # the next two lines are only needed for 25.2, which had + # some instability in maintaining the node_ids + rev = emit_app.results.analyze() + emitter_node = rev.get_component_node(emitter_name) + emit_app.schematic.delete_component(emitter_node.name) + + try: + emit_app.schematic.delete_component("Dummy Comp") + except RuntimeError: + print("Invalid component can't be deleted.") + + @pytest.mark.skipif(config["desktopVersion"] < "2025.2", reason="Skipped on versions earlier than 2025 R2.") + def test_exceptions(self, emit_app): + radio: RadioNode = emit_app.schematic.create_component("New Radio", "Radios") + + try: + radio._get_node(-1) + except Exception as e: + print(f"Invalid {e}") + + try: + radio._set_property("Bad Prop", "Bad Val") + except Exception as e: + print(f"Error: {e}") + + try: + radio._get_property("Bad Prop") + except Exception as e: + print(f"Error: {e}") + + band: Band = radio.children[0] + try: + band.start_frequency = "100 Gbps" + except Exception as e: + print(f"Invalid units: {e}") + + try: + radio._get_child_node_id("Bad Node Name") + except Exception as e: + print(f"Invalid child node name: {e}") + + try: + radio._add_child_node("Bad Type") + except Exception as e: + print(f"Invalid child type: {e}") + + @pytest.mark.skipif(config["desktopVersion"] <= "2025.1", reason="Skipped on versions earlier than 2026 R1.") + def test_units(self, emit_app): + new_radio = emit_app.schematic.create_component("New Radio") + band_node = [band for band in new_radio.children if "Band" == band.node_type][0] + band_node = cast(Band, band_node) + band_node.modulation = Band.ModulationOption.MSK + band_node.bit_rate = "600 bps" + assert band_node.bit_rate == 600.0 + + band_node.bit_rate = "600 kbps" + assert band_node.bit_rate == 600000.0 + + band_node.bit_rate = "600 Mbps" + assert band_node.bit_rate == 600000000.0 + + band_node.bit_rate = "600 Gbps" + assert band_node.bit_rate == 600000000000.0 + + band_node.bit_rate = 500 + assert band_node.bit_rate == 500.0 + + band_node.bit_rate = "750" + assert band_node.bit_rate == 750.0 + + band_node.stop_frequency = 2000000000 + assert band_node.stop_frequency == 2000000000.0 + + band_node.stop_frequency = "1000000000" + assert band_node.stop_frequency == 1000000000.0 + + band_node.start_frequency = "100 MHz" + assert band_node.start_frequency == 100000000.0 + + band_node.start_frequency = "200MHz" + assert band_node.start_frequency == 200000000.0 + + cable = emit_app.schematic.create_component("Cable") + cable.length = "5.4681 yd" + assert round(cable.length, 4) == 5.0000 + + cable.length = "0.0031 mile" + assert round(cable.length, 4) == 4.9890 + + @pytest.mark.skipif(config["desktopVersion"] < "2026.1", reason="Skipped on versions earlier than 2026 R1.") def test_27_components_catalog(self, emit_app): comp_list = emit_app.modeler.components.components_catalog["LTE"] assert len(comp_list) == 14 assert comp_list[12].name == "LTE BTS" assert comp_list[13].name == "LTE Mobile Station" + + # test that every EMIT component can be added to the schematic + # Components_catalog returns a list in the form Library:CompName + comp_list = emit_app.modeler.components.components_catalog + + # create a default radio and antenna to use for testing connections + default_radio = emit_app.schematic.create_component("New Radio") + default_antenna = emit_app.schematic.create_component("Antenna") + + for comp in comp_list.components: + library_name = comp.split(":")[0] + comp_to_add = comp.split(":")[1] + # try to add just based on the CompName + try: + comp_added = emit_app.schematic.create_component(component_type=comp_to_add, library=library_name) + assert comp_added + + # connect the component + if isinstance(comp_added, EmitterNode): + # can't connect Emitters since they have no ports + emit_app.schematic.delete_component(comp_added.name) + continue + elif isinstance(comp_added, AntennaNode) or isinstance(comp_added, Terminator): + emit_app.schematic.connect_components(default_radio.name, comp_added.name) + else: + emit_app.schematic.connect_components(comp_added.name, default_antenna.name) + + # Delete the component + print(comp_added.name) + emit_app.schematic.delete_component(comp_added.name) + + except Exception as e: + print(f"Failed to create component: {comp_to_add} from library {library_name}. Error: {e}") + + rev = emit_app.results.analyze() + comps_in_schematic = rev.get_all_component_nodes() + assert len(comps_in_schematic) == 2 # default antenna/radio should remain