Skip to content

FEAT: emit_schematic module is added. #6240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/6240.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Emit_schematic module is added.
13 changes: 13 additions & 0 deletions src/ansys/aedt/core/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from ansys.aedt.core.emit_core.couplings import CouplingsEmit
from ansys.aedt.core.emit_core.emit_constants import EMIT_VALID_UNITS
from ansys.aedt.core.emit_core.emit_constants import emit_unit_type_string_to_enum
from ansys.aedt.core.emit_core.emit_schematic import EmitSchematic
from ansys.aedt.core.emit_core.results.results import Results
from ansys.aedt.core.generic.general_methods import pyaedt_function_handler
from ansys.aedt.core.modeler.schematic import ModelerEmit
Expand Down Expand Up @@ -172,6 +173,7 @@
)
self._modeler = ModelerEmit(self)
self._couplings = CouplingsEmit(self)
self._schematic = EmitSchematic(self)
if self._aedt_version > "2023.1":
# the next 2 lines of code are needed to point
# the EMIT object to the correct EmiApiPython
Expand Down Expand Up @@ -216,6 +218,17 @@
"""
return self._couplings

@property
def schematic(self):
"""EMIT Schematic.

Returns
-------
:class:`ansys.aedt.core.emit_core.emit_schematic.EmitSchematic`
EMIT schematic.
"""
return self._schematic

Check warning on line 230 in src/ansys/aedt/core/emit.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit.py#L230

Added line #L230 was not covered by tests

@pyaedt_function_handler()
def version(self, detailed=False):
"""
Expand Down
238 changes: 238 additions & 0 deletions src/ansys/aedt/core/emit_core/emit_schematic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# -*- 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.


from ansys.aedt.core.generic.general_methods import pyaedt_function_handler


class EmitSchematic:
"""Represents the EMIT schematic and provides methods to interact with it."""

def __init__(self, emit_instance):
"""Initialize the EmitSchematic class.

Parameters
----------
emit_instance : Emit
Instance of the Emit class.
"""
self.emit_instance = emit_instance

@property
def _emit_com_module(self):
"""Retrieve the EmitCom module from the Emit instance.

Returns
-------
object
The EmitCom module.

Raises
------
RuntimeError
If the EmitCom module cannot be retrieved.
"""
if not hasattr(self.emit_instance, "_odesign"):
raise RuntimeError("Emit instance does not have a valid '_odesign' attribute.")
try:
return self.emit_instance._odesign.GetModule("EmitCom")
except Exception as e:
raise RuntimeError(f"Failed to retrieve EmitCom module: {e}")

Check warning on line 61 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L56-L61

Added lines #L56 - L61 were not covered by tests

@pyaedt_function_handler
def create_component(self, component_type: str, name: str = None, library: str = None) -> int:
"""Create a component.

Parameters
----------
component_type : str
Type of the component to create.
name : str, optional
Name of the component to create. AEDT defaults used if not provided.
library : str, optional
Name of the component library. Defaults to an empty string if not provided.

Returns
-------
int
The ID of the created component.

Raises
------
ValueError
If the component type is empty or no matching component is found.
RuntimeError
If the component creation fails.
"""
if not component_type:
raise ValueError("The 'component_type' argument is required.")

Check warning on line 89 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L88-L89

Added lines #L88 - L89 were not covered by tests

name = name or ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never knew you could do this, nice!

library = library or ""

Check warning on line 92 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L91-L92

Added lines #L91 - L92 were not covered by tests

try:

Check warning on line 94 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L94

Added line #L94 was not covered by tests
# Retrieve matching components from the catalog
matching_components = self.emit_instance.modeler.components.components_catalog[component_type]

Check warning on line 96 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L96

Added line #L96 was not covered by tests

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}'.")

Check warning on line 100 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L98-L100

Added lines #L98 - L100 were not covered by tests

if len(matching_components) == 1:

Check warning on line 102 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L102

Added line #L102 was not covered by tests
# Use the single matching component
component = matching_components[0]
self.emit_instance.logger.info(

Check warning on line 105 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L104-L105

Added lines #L104 - L105 were not covered by tests
f"Using component '{component.name}' from library '{component.component_library}"
"' for type '{component_type}'."
)
else:
# Attempt to find an exact match
component = next((comp for comp in matching_components if comp.name == component_type), None)
if not component:
self.emit_instance.logger.error(

Check warning on line 113 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L111-L113

Added lines #L111 - L113 were not covered by tests
f"Multiple components found for type '{component_type}', but no exact match."
" Please specify a unique component."
)
raise ValueError(f"Multiple components found for type '{component_type}', but no exact match.")
self.emit_instance.logger.info(

Check warning on line 118 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L117-L118

Added lines #L117 - L118 were not covered by tests
f"Using exact match component '{component.name}' from library '{component.component_library}"
"' for type '{component_type}'."
)

# Create the component using the EmitCom module
new_component_id = self._emit_com_module.CreateEmitComponent(

Check warning on line 124 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L124

Added line #L124 was not covered by tests
name, component.name, component.component_library
)
return new_component_id
except Exception as e:
self.emit_instance.logger.error(f"Failed to create component '{name}' of type '{component_type}': {e}")
raise RuntimeError(f"Failed to create component of type '{component_type}': {e}")

Check warning on line 130 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L127-L130

Added lines #L127 - L130 were not covered by tests

@pyaedt_function_handler
def create_radio_antenna(
self, radio_type: str, radio_name: str = None, antenna_name: str = None, library: str = None
) -> tuple[int, int]:
"""Create a new radio and antenna and connect them.

Parameters
----------
radio_type : str
Type of radio to create. For example, "Bluetooth". Must match
a radio name in the specified library.
radio_name : str, optional
Name to assign to the new radio. If ``None``, then an instance
name is assigned automatically. The default is ``None``.
antenna_name : str, optional
Name to assign to the new antenna. If ``None``, then an instance
name is assigned automatically. The default is ``None``.
library : str, optional
Name of the component library. If ``None``, then the default
library is used. The default is ``None``.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If None is specified, it'll search all of EMIT Elements, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The radio_type cannot be None, but for a given keyword, it looks for the library, so the library could be None for ease of use.


Returns
-------
tuple
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with create_component(), I think it would be better to return the nodes for each component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBD.

A tuple containing the IDs of the created radio and antenna.

Raises
------
RuntimeError
If the radio or antenna creation fails.
"""
radio_name = radio_name or ""
antenna_name = antenna_name or ""
library = library or ""

Check warning on line 165 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L163-L165

Added lines #L163 - L165 were not covered by tests

try:
new_radio_id = self.create_component(radio_type, radio_name, library)
new_antenna_id = self.create_component("Antenna", antenna_name, "Antennas")
if new_radio_id and new_antenna_id:
self.connect_components(new_antenna_id, new_radio_id) # Connect antenna to radio
return new_radio_id, new_antenna_id
except Exception as e:
self.emit_instance.logger.error(f"Failed to create radio of type '{radio_type}' or antenna: {e}")
raise RuntimeError(f"Failed to create radio of type '{radio_type}' or antenna: {e}")

Check warning on line 175 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L167-L175

Added lines #L167 - L175 were not covered by tests

@pyaedt_function_handler
def connect_components(self, component_id_1: int, component_id_2: int):
"""Connect two components in the schematic.

Parameters
----------
component_id_1 : str
ID of the first component.
component_id_2 : str
ID of the second component.

Raises
------
RuntimeError
If the connection fails.
"""
try:
component_name_1 = self.get_component_properties(component_id_1, "Name")
component_name_2 = self.get_component_properties(component_id_2, "Name")
self.emit_instance._oeditor.PlaceComponent(component_name_1, component_name_2)
self.emit_instance.logger.info(

Check warning on line 197 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L193-L197

Added lines #L193 - L197 were not covered by tests
f"Successfully connected components '{component_name_1}' and '{component_name_2}'."
)
except Exception as e:
self.emit_instance.logger.error(

Check warning on line 201 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L200-L201

Added lines #L200 - L201 were not covered by tests
f"Failed to connect components '{component_id_1}' and '{component_id_2}': {e}"
)
raise RuntimeError(f"Failed to connect components '{component_id_1}' and '{component_id_2}': {e}")

Check warning on line 204 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L204

Added line #L204 was not covered by tests

@pyaedt_function_handler
def get_component_properties(self, component_id: int, property_key: str = None) -> dict:
"""Get properties of a component.

Parameters
----------
component_id : int
ID of the component.
property_key : str, optional
Specific property key to retrieve. If ``None``, all properties are returned.

Returns
-------
dict or str
Dictionary containing all properties of the component if `property_key` is ``None``.
Otherwise, the value of the specified property key.

Raises
------
KeyError
If the specified property key is not found.
"""
try:
props = self._emit_com_module.GetEmitNodeProperties(0, component_id, True)
props_dict = {prop.split("=", 1)[0]: prop.split("=", 1)[1] for prop in props}
if property_key is None:
return props_dict
if property_key in props_dict:
return props_dict[property_key]
raise KeyError(f"Property key '{property_key}' not found.")
except Exception as e:
self.emit_instance.logger.error(f"Failed to retrieve properties for component '{component_id}': {e}")
raise RuntimeError(f"Failed to retrieve properties for component '{component_id}': {e}")

Check warning on line 238 in src/ansys/aedt/core/emit_core/emit_schematic.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/emit_core/emit_schematic.py#L228-L238

Added lines #L228 - L238 were not covered by tests
72 changes: 70 additions & 2 deletions tests/system/solvers/test_26_emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import os
import sys
import tempfile
from unittest.mock import MagicMock

import pytest

Expand Down Expand Up @@ -67,7 +68,7 @@ def aedtapp(add_app):
(sys.version_info < (3, 10) or sys.version_info > (3, 12)) and config["desktopVersion"] > "2024.2",
reason="Emit API is only available for Python 3.10-3.12 in AEDT versions 2025.1 and later.",
)
@pytest.mark.skipif(config["desktopVersion"] == "2025.2", reason="WAITING")
# @pytest.mark.skipif(config["desktopVersion"] == "2025.2", reason="WAITING")
class TestClass:
@pytest.fixture(autouse=True)
def init(self, aedtapp, local_scratch):
Expand Down Expand Up @@ -1294,10 +1295,77 @@ def count_license_actions(license_file_path):

assert checkouts == expected_checkouts and checkins == expected_checkins

@pytest.mark.skipif(config["desktopVersion"] < "2025.1", reason="Skipped on versions earlier than 2024 R2.")
@pytest.mark.skipif(config["desktopVersion"] < "2022.2", reason="Skipped on versions earlier than 2025 R2.")
def test_25_components_catalog(self, add_app):
self.aedtapp = add_app(project_name="catalog-list", application=Emit)
comp_list = self.aedtapp.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"

@pytest.mark.skipif(config["desktopVersion"] < "2025.2", reason="Skipped on versions earlier than 2025 R2.")
def test_26_create_component(self, add_app):
self.aedtapp = add_app(project_name="create_component", application=Emit)
self.aedtapp.logger.info = MagicMock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bryankaylor Never seen this before, but maybe it can help us with the general node unit testing?

new_radio = self.aedtapp.schematic.create_component("MICS")
assert new_radio == 3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should double check with Apollo, but I don't think the node ID values are guaranteed. If nodes are somehow created in a different order, the node ID could change.

Granted, if you update to return the node, then you can instead assert new_radio.name = 'MICS' (or whatever it's supposed to be).

self.aedtapp.logger.info.assert_called_with(
r"Using component 'MICS' from library 'Radios\Commercial Unlicensed Systems\Medical' for type 'MICS'."
)
with pytest.raises(TypeError) as e:
self.aedtapp.schematic.create_component()
assert "EmitSchematic.create_component() missing 1 required positional argument: 'component_type'" in str(
e.value
)
with pytest.raises(RuntimeError) as e:
self.aedtapp.schematic.create_component("WrongComponent")
assert (
"Failed to create component of type 'WrongComponent': No component found for type 'WrongComponent'."
) in str(e.value)
with pytest.raises(RuntimeError) as e:
self.aedtapp.schematic.create_component("lte")
assert (
"Failed to create component of type 'lte': Multiple components found for type 'lte', but no exact match."
) in str(e.value)

@pytest.mark.skipif(config["desktopVersion"] < "2025.2", reason="Skipped on versions earlier than 2025 R2.")
def test_27_create_radio_antenna(self, add_app):
self.aedtapp = add_app(project_name="radio_antenna", application=Emit)
new_radio, new_antenna = self.aedtapp.schematic.create_radio_antenna("MICS", "Radio", "Antenna")
assert new_radio == 3
assert new_antenna == 4
Comment on lines +1335 to +1336
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert new_radio == 3
assert new_antenna == 4
assert new_radio == 3
assert new_antenna == 4

Same comment as above

with pytest.raises(RuntimeError) as e:
self.aedtapp.schematic.create_radio_antenna("WrongComponent", "Radio", "Antenna")
assert "Failed to create radio of type 'WrongComponent'" in str(e.value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any error cases to test for create_radio_antenna?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a test :
with pytest.raises(RuntimeError) as e:
self.aedtapp.schematic.create_radio_antenna("WrongComponent", "Radio", "Antenna")
assert "Failed to create radio of type 'WrongComponent'" in str(e.value)


@pytest.mark.skipif(config["desktopVersion"] < "2025.2", reason="Skipped on versions earlier than 2025 R2.")
def test_28_connect_components(self, add_app):
self.aedtapp = add_app(project_name="connect_components", application=Emit)
self.aedtapp.logger.info = MagicMock()
new_radio = self.aedtapp.schematic.create_component("MICS")
new_antenna = self.aedtapp.schematic.create_component("Antenna")
self.aedtapp.schematic.connect_components(new_radio, new_antenna)
self.aedtapp.logger.info.assert_called_with("Successfully connected components 'MICS' and 'Antenna'.")
with pytest.raises(RuntimeError) as e:
self.aedtapp.schematic.connect_components(new_radio, 6)
assert (
"Failed to connect components '3' and '6': Failed to retrieve properties for component '6': "
"Failed to execute gRPC AEDT command: GetEmitNodeProperties"
) in str(e.value)

@pytest.mark.skipif(config["desktopVersion"] < "2025.2", reason="Skipped on versions earlier than 2025 R2.")
def test_29_get_component_properties(self, add_app):
self.aedtapp = add_app(project_name="component_properties", application=Emit)
new_radio = self.aedtapp.schematic.create_component("MICS")
new_radio_props = self.aedtapp.schematic.get_component_properties(new_radio)
assert isinstance(new_radio_props, dict)
assert new_radio_props["Name"] == "MICS"
assert new_radio_props["Type"] == "RadioNode"
assert new_radio_props["IconAlias"] == ":Radio"
new_radio_name = self.aedtapp.schematic.get_component_properties(new_radio, "Name")
assert new_radio_name == "MICS"
with pytest.raises(RuntimeError) as e:
self.aedtapp.schematic.get_component_properties(new_radio, "WrongProp")
assert ("Failed to retrieve properties for component '3': \"Property key 'WrongProp' not found.\"") in str(
e.value
)