diff --git a/doc/changelog.d/5920.added.md b/doc/changelog.d/5920.added.md new file mode 100644 index 00000000000..a3ada54a8a2 --- /dev/null +++ b/doc/changelog.d/5920.added.md @@ -0,0 +1 @@ +circuit configuration \ No newline at end of file diff --git a/src/ansys/aedt/core/application/analysis_nexxim.py b/src/ansys/aedt/core/application/analysis_nexxim.py index dcd7876721f..1b09aff7b5b 100644 --- a/src/ansys/aedt/core/application/analysis_nexxim.py +++ b/src/ansys/aedt/core/application/analysis_nexxim.py @@ -24,6 +24,7 @@ import warnings from ansys.aedt.core.application.analysis import Analysis +from ansys.aedt.core.generic.configurations import ConfigurationsNexxim from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.settings import settings from ansys.aedt.core.modeler.circuits.object_3d_circuit import CircuitComponent @@ -91,10 +92,21 @@ def __init__( self._post = None self._internal_excitations = None self._internal_sources = None + self._configurations = ConfigurationsNexxim(self) if not settings.lazy_load: self._modeler = self.modeler self._post = self.post + @property + def configurations(self): + """Property to import and export configuration files. + + Returns + ------- + :class:`ansys.aedt.core.generic.configurations.Configurations` + """ + return self._configurations + @pyaedt_function_handler(setupname="name") def delete_setup(self, name): """Delete a setup. diff --git a/src/ansys/aedt/core/generic/configurations.py b/src/ansys/aedt/core/generic/configurations.py index 78091b1bb0b..c911c82dbf4 100644 --- a/src/ansys/aedt/core/generic/configurations.py +++ b/src/ansys/aedt/core/generic/configurations.py @@ -22,6 +22,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from collections import defaultdict import copy from datetime import datetime import json @@ -2166,3 +2167,223 @@ def apply_operations_to_native_components(obj, operation_dict, native_dict): # native_dict["Instances"], ) return True + + +class ConfigurationsNexxim(Configurations): + """Enables export and import configuration options to be applied to a new or existing Nexxim design.""" + + @pyaedt_function_handler() + def export_config(self, config_file=None, overwrite=False): + """Export current design properties to a JSON or TOML file. + + Parameters + ---------- + config_file : str, optional + Full path to json file. If ``None``, then the config file will be saved in working directory. + overwrite : bool, optional + If ``True`` the json file will be overwritten if already existing. + If ``False`` and the version is compatible, the data in the existing file will be updated. + Default is ``False``. + + Returns + ------- + str + Exported config file. + """ + + if not config_file: + config_file = os.path.join( + self._app.working_directory, generate_unique_name(self._app.design_name) + ".json" + ) + # dict_out = {} + # self._export_general(dict_out) + pin_mapping = defaultdict(list) + data_refdes = {} + data_models = {} + pin_nets = {} + skip_list = [ + "LabelID", + "ADD_NOISE", + "DTEMP", + "ModelName", + "CosimDefinition", + "CoSimulator", + "InstanceName", + "NexximNetlist", + "Name", + "COMPONENT", + "EyeMeasurementFunctions", + "ACMAG", + ] + for comp in list(self._app.modeler.schematic.components.values()): + properties = {} + num_terminals = None + refdes = comp.refdes + position = comp.location + angle = comp.angle + parameters = comp.parameters + if not comp.component_info: + continue + else: + component = comp.component_info["Component"] + path = comp.component_path + if not path: + component_type = "Nexxim Component" + path = "" + for param, value in parameters.items(): + if param in skip_list: + continue + elif value and value[-1] == "'" and value[1] == "'": + value = value[-1:1] + properties[param] = value + elif path[-4:] == ".ibs": + if "AMI_Version" in parameters: + component_type = "ami" + else: + component_type = "ibis" + for prop, value in parameters.items(): + if value and value[-1] == '"' and value[0] == '"': + value = value[1:-1] + properties[prop] = value + elif path[-4:] in [".LIB", ".lib"] or path[-3:] == ".sp": + component_type = "spice" + elif path[-1:] == "p" and path[-2:-1].isdigit(): + component_type = "touchstone" + elif path[-4:] == ".sss": + component_type = "nexxim state space" + num_terminals = comp.model_data.props["numberofports"] + + for pin in comp.pins: + if pin.net == "0": + net = "gnd" + else: + net = pin.net + temp_dict = {pin: net} + pin_nets.update(temp_dict) + + temp_dict2 = { + refdes: {"component": component, "properties": properties, "position": position, "angle": angle} + } + data_refdes.update(temp_dict2) + if num_terminals: + model = { + component: {"component_type": component_type, "file_path": path, "num_terminals": num_terminals} + } + num_terminals = None + else: + model = {component: {"component_type": component_type, "file_path": path}} + data_models.update(model) + + for k, v in pin_nets.items(): + pin_mapping[v].append(k) + + if "" in pin_mapping: + del pin_mapping[""] + for k, l in pin_mapping.items(): + temp_dict3 = {} + for i in l: + temp_dict3.update({i._circuit_comp.refdes: i.name}) + pin_mapping[k] = temp_dict3 + + dict_out = { + "models": data_models, + "refdes": data_refdes, + "pin_mapping": pin_mapping, + } # Call private export method to update dict_out. + + # update the json if it exists already + + if os.path.exists(config_file) and not overwrite: + dict_in = read_configuration_file(config_file) + try: # TODO: Allow import of config created with other versions of pyaedt. + if dict_in["general"]["pyaedt_version"] == __version__: + for k, v in dict_in.items(): + if k not in dict_out: + dict_out[k] = v + elif isinstance(v, dict): + for i, j in v.items(): + if i not in dict_out[k]: + dict_out[k][i] = j + except KeyError as e: + self._app.logger.error(str(e)) + + # write the updated dict to file + if write_configuration_file(dict_out, config_file): + self._app.logger.info(f"Json file {config_file} created correctly.") + return config_file + self._app.logger.error(f"Error creating json file {config_file}.") + return False + + @pyaedt_function_handler() + def import_config(self, config_file, *args): + """Import configuration settings from a JSON or TOML file and apply it to the current design. + + + Parameters + ---------- + config_file : str + Full path to json file. + + Returns + ------- + dict, bool + Config dictionary. + """ + if len(args) > 0: # pragma: no cover + raise TypeError("import_config expected at most 1 arguments, got %d" % (len(args) + 1)) + self.results._reset_results() + + data = read_configuration_file(config_file) + for i, j in data["refdes"].items(): + for k, l in data["models"].items(): + if k == j["component"]: + component_type = l["component_type"] + if component_type == "Nexxim Component": + new_comp = self._app.modeler.components.create_component( + name=i, + component_library="", + component_name=j["component"], + location=j["position"], + angle=j["angle"], + ) + elif component_type in ["ibis", "ami"]: + if component_type == "ami": + ami = True + else: + ami = False + comp_set = self._app.get_ibis_model_from_file(l["file_path"], ami).components.values() + for comp in comp_set: + for pin in comp.pins.values(): + if pin.buffer_name == k: + new_comp = pin.insert(j["position"][0], j["position"][1]) + elif component_type == "touchstone": + new_comp = self._app.modeler.schematic.create_touchstone_component( + l["file_path"], location=j["position"], angle=j["angle"] + ) + elif component_type == "spice": + new_comp = self._app.modeler.schematic.create_component_from_spicemodel( + input_file=l["file_path"], location=j["position"] + ) + elif component_type == "nexxim state space": + new_comp = self._app.modeler.schematic.create_nexxim_state_space_component( + l["file_path"], l["num_terminals"], location=j["position"], angle=j["angle"] + ) + for name, parameter in j["properties"].items(): + new_comp.parameters[name] = parameter + + for i, j in data["pin_mapping"].items(): + pins = [] + for k, l in j.items(): + for comp in list(self._app.modeler.schematic.components.values()): + if not comp.refdes: + continue + elif comp.refdes == k: + for pin in comp.pins: + if pin.name == l: + pins.append(pin) + if i == "gnd": + for gnd_pin in pins: + self._app.modeler.schematic.create_gnd(gnd_pin.location, gnd_pin.angle, page=i) + else: + pins[0].connect_to_component(pins[1:], page_name=i) + return data diff --git a/src/ansys/aedt/core/generic/ibis_reader.py b/src/ansys/aedt/core/generic/ibis_reader.py index 7c94c87d702..c2709b5038a 100644 --- a/src/ansys/aedt/core/generic/ibis_reader.py +++ b/src/ansys/aedt/core/generic/ibis_reader.py @@ -112,10 +112,11 @@ class Pin: Circuit in which the pin will be added to. """ - def __init__(self, name, buffername, circuit): + def __init__(self, name, buffername, app): self._name = name self._buffer_name = buffername - self._circuit = circuit + self._app = app + self._circuit = app.circuit self._short_name = None self._signal = None self._model = None @@ -262,6 +263,9 @@ def c_value(self, value): def add(self): """Add a pin to the list of components in the Project Manager.""" try: + available_names = self._circuit.modeler.schematic.ocomponent_manager.GetNames() + if self.name not in available_names: + self._app._app.import_model_in_aedt() return self._circuit.modeler.schematic.ocomponent_manager.AddSolverOnDemandModel( self.buffer_name, [ @@ -302,7 +306,8 @@ def insert(self, x, y, angle=0.0): Circuit Component Object. """ - + if self.buffer_name not in self._circuit.modeler.schematic.ocomponent_manager.GetNames(): + self._app._app.import_model_in_aedt() return self._circuit.modeler.schematic.create_component( component_library=None, component_name=self.buffer_name, location=[x, y], angle=angle ) @@ -319,9 +324,10 @@ class DifferentialPin: Circuit to add the pin to. """ - def __init__(self, name, buffer_name, circuit): + def __init__(self, name, buffer_name, app): self._buffer_name = buffer_name - self._circuit = circuit + self._app = app + self._circuit = app._circuit self._name = name self._tdelay_min = None self._tdelay_max = None @@ -423,6 +429,9 @@ def name(self): def add(self): """Add a pin to the list of components in the Project Manager.""" try: + available_names = self._circuit.modeler.schematic.ocomponent_manager.GetNames() + if self.buffer_name not in available_names: + self._app.import_model_in_aedt() return self._circuit.modeler.schematic.ocomponent_manager.AddSolverOnDemandModel( self.buffer_name, [ @@ -463,17 +472,19 @@ def insert(self, x, y, angle=0.0): Circuit Component Object. """ - + if self.buffer_name not in self._circuit.modeler.schematic.ocomponent_manager.GetNames(): + self._app.import_model_in_aedt() return self._circuit.modeler.schematic.create_component( component_library=None, component_name=self.buffer_name, location=[x, y], angle=angle ) class Buffer: - def __init__(self, ibis_name, short_name, circuit): + def __init__(self, ibis_name, short_name, app): self._ibis_name = ibis_name self._short_name = short_name - self._circuit = circuit + self._app = app + self._circuit = app._circuit @property def name(self): @@ -487,6 +498,9 @@ def short_name(self): def add(self): """Add a buffer to the list of components in the Project Manager.""" + available_names = self._circuit.modeler.schematic.ocomponent_manager.GetNames() + if self.name not in available_names: + self._app.import_model_in_aedt() self._circuit.modeler.schematic.ocomponent_manager.AddSolverOnDemandModel( self.name, [ @@ -524,7 +538,8 @@ def insert(self, x, y, angle=0.0): Circuit Component Object. """ - + if self.name not in self._circuit.modeler.schematic.ocomponent_manager.GetNames(): + self._app.import_model_in_aedt() return self._circuit.modeler.schematic.create_component( component_library=None, component_name=self.name, location=[x, y], angle=angle ) @@ -654,8 +669,9 @@ class Ibis: """ # Ibis reader must work independently or in Circuit. - def __init__(self, name, circuit): - self.circuit = circuit + def __init__(self, name, app): + self._app = app + self.circuit = app._circuit self._name = name self._components = {} self._model_selectors = [] @@ -715,8 +731,9 @@ class AMI: """ # Ibis reader must work independently or in Circuit. - def __init__(self, name, circuit): - self.circuit = circuit + def __init__(self, name, app): + self._app = app + self.circuit = app._circuit self._name = name self._components = {} self._model_selectors = [] @@ -814,7 +831,7 @@ def parse_ibis_file(self): raise Exception(f"{self._filename} does not exist.") ibis_name = get_filename_without_extension(self._filename) - ibis = Ibis(ibis_name, self._circuit) + ibis = Ibis(ibis_name, self) check_and_download_file(self._filename) @@ -834,20 +851,31 @@ def parse_ibis_file(self): buffers = {} for model_selector in ibis.model_selectors: - buffer = Buffer(ibis_name, model_selector.name, self._circuit) + buffer = Buffer(ibis_name, model_selector.name, self) buffers[buffer.name] = buffer for model in ibis.models: - buffer = Buffer(ibis_name, model.name, self._circuit) + buffer = Buffer(ibis_name, model.name, self) buffers[buffer.name] = buffer ibis.buffers = buffers self._ibis_model = ibis - available_names = self._circuit.modeler.schematic.ocomponent_manager.GetNames() - already_present = [i for i in buffers.keys() if i in available_names] - if len(already_present) == len(buffers): - return ibis_info + return ibis_info + + def import_model_in_aedt(self): + """Check and import the ibis model in AEDT. + + Returns + ------- + bool + ``True`` when the model is imported successfully, ``False`` if not imported or model already present. + + """ + + if [i for i in self._circuit.modeler.schematic.ocomponent_manager.GetNames() if i in self._ibis_model.buffers]: + return False + if self._circuit: args = [ "NAME:Options", @@ -861,12 +889,12 @@ def parse_ibis_file(self): False, ] arg_buffers = ["NAME:Buffers"] - for buffer_item in buffers.values(): + for buffer_item in self._ibis_model.buffers.values(): arg_buffers.append(f"{buffer_item.short_name}:=") arg_buffers.append([True, "IbisSingleEnded"]) - model_selector_names = [i.name for i in ibis.model_selectors] + model_selector_names = [i.name for i in self._ibis_model.model_selectors] arg_components = ["NAME:Components"] - for comp_value in ibis.components.values(): + for comp_value in self._ibis_model.components.values(): arg_component = [f"NAME:{comp_value.name}"] for pin in comp_value.pins.values(): arg_component.append(f"{pin.short_name}:=") @@ -874,14 +902,17 @@ def parse_ibis_file(self): arg_component.append([False, False]) else: arg_component.append([True, False]) + if hasattr(pin, "negative_pin"): + arg_component.append(f"{pin.short_name}:=") + arg_component.append([True, True]) arg_components.append(arg_component) args.append(arg_buffers) args.append(arg_components) self._circuit.modeler.schematic.ocomponent_manager.ImportModelsFromFile(str(self._filename), args) - - return ibis_info + return True + return False # Model def read_model(self, ibis, model_list): @@ -898,7 +929,11 @@ def read_model(self, ibis, model_list): """ for model_info in model_list: - model_spec_info = model_info["model"].strip().split("\n") + if "model" in model_info: + model_spec_info = model_info["model"].strip().split("\n") + elif "model selector" in model_info: + model_spec_info = model_info["model selector"].strip().split("\n") + model_spec_info = [i.split(" ")[0] for i in model_spec_info] for idx, model_spec in enumerate(model_spec_info): if not idx: model = Model() @@ -1097,7 +1132,7 @@ def make_diff_pin_object(self, line, component, ibis): diff_pin_name = single_ended_pin_name + "_diff" for pin_name, pinval in component.pins.items(): if single_ended_pin_name == pin_name: - pin = DifferentialPin(diff_pin_name, pinval.buffer_name + "_diff", pinval._circuit) + pin = DifferentialPin(diff_pin_name, pinval.buffer_name + "_diff", self) pin._short_name = pinval.short_name pin._tdelay_max = tdelay_max pin._tdelay_min = tdelay_min @@ -1151,7 +1186,7 @@ def make_pin_object(self, line, component_name, ibis): pin = Pin( pin_name + "_" + component_name + "_" + ibis.name, signal + "_" + component_name + "_" + ibis.name, - ibis.circuit, + ibis, ) pin.short_name = pin_name pin.signal = signal @@ -1230,7 +1265,7 @@ def parse_ibis_file(self): raise Exception(f"{self._filename} does not exist.") ami_name = get_filename_without_extension(self._filename) - ibis = AMI(ami_name, self._circuit) + ibis = AMI(ami_name, self) check_and_download_file(self._filename) # Read *.ibis file. @@ -1249,15 +1284,22 @@ def parse_ibis_file(self): buffers = {} for model_selector in ibis.model_selectors: - buffer = Buffer(ami_name, model_selector.name, self._circuit) + buffer = Buffer(ami_name, model_selector.name, self) buffers[buffer.name] = buffer for model in ibis.models: - buffer = Buffer(ami_name, model.name, self._circuit) + buffer = Buffer(ami_name, model.name, self) buffers[buffer.name] = buffer ibis.buffers = buffers + self._ibis_model = ibis + return ibis_info + + def import_model_in_aedt(self): + + if [i for i in self._circuit.modeler.schematic.ocomponent_manager.GetNames() if i in self._ibis_model.buffers]: + return False if self._circuit: args = [ "NAME:Options", @@ -1271,22 +1313,28 @@ def parse_ibis_file(self): False, ] arg_buffers = ["NAME:Buffers"] - for buffer in buffers: - arg_buffers.append(f"{buffers[buffer].short_name}:=") + for buffer in self._ibis_model.buffers: + arg_buffers.append(f"{self._ibis_model.buffers[buffer].short_name}:=") arg_buffers.append([True, "IbisSingleEnded"]) - model_selector_names = [i.name for i in ibis.model_selectors] + model_selector_names = [i.name for i in self._ibis_model.model_selectors] arg_components = ["NAME:Components"] - for component in ibis.components: - arg_component = [f"NAME:{ibis.components[component].name}"] - for pin in ibis.components[component].pins: - arg_component.append(f"{ibis.components[component].pins[pin].short_name}:=") + for component in self._ibis_model.components: + arg_component = [f"NAME:{self._ibis_model.components[component].name}"] + for pin in self._ibis_model.components[component].pins: + arg_component.append(f"{self._ibis_model.components[component].pins[pin].short_name}:=") flag = True - if not isinstance(ibis.components[component].pins[pin], DifferentialPin): + if not isinstance(self._ibis_model.components[component].pins[pin], DifferentialPin): flag = False - if model_selector_names and ibis.components[component].pins[pin].model not in model_selector_names: + if ( + model_selector_names + and self._ibis_model.components[component].pins[pin].model not in model_selector_names + ): arg_component.append([False, flag]) else: arg_component.append([True, flag]) + if hasattr(pin, "negative_pin"): + arg_component.append(f"{pin.short_name}:=") + arg_component.append([True, True]) arg_components.append(arg_component) args.append(arg_buffers) @@ -1294,9 +1342,6 @@ def parse_ibis_file(self): self._circuit.modeler.schematic.ocomponent_manager.ImportModelsFromFile(self._filename, args) - self._ibis_model = ibis - return ibis_info - def is_started_with(src, find, ignore_case=True): """Verify if a string content starts with a specific string or not. diff --git a/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py b/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py index 1a97609695f..c8b8b2cc47f 100644 --- a/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py +++ b/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py @@ -91,9 +91,7 @@ def net(self): for net in self._circuit_comp._circuit_components.nets: conns = self._oeditor.GetNetConnections(net) for conn in conns: - if conn.endswith(self.name) and ( - f";{self._circuit_comp.id};" in conn or f";{self._circuit_comp.id} " in conn - ): + if self._circuit_comp.composed_name in conn and conn.endswith(self.name): return net return "" diff --git a/src/ansys/aedt/core/modeler/circuits/primitives_circuit.py b/src/ansys/aedt/core/modeler/circuits/primitives_circuit.py index fa38a79f841..12ce634241e 100644 --- a/src/ansys/aedt/core/modeler/circuits/primitives_circuit.py +++ b/src/ansys/aedt/core/modeler/circuits/primitives_circuit.py @@ -769,7 +769,7 @@ def create_model_from_nexxim_state_space(self, input_file, num_terminal, model_n if model_name in list(self.omodel_manager.GetNames()): model_name = generate_unique_name(model_name, n=2) - port_names = ["Port" + str(i + 1) for i in range(num_terminal)] + port_names = [str(i + 1) for i in range(num_terminal)] arg = [ "NAME:" + model_name, "Name:=", diff --git a/tests/system/general/test_01_configuration_files.py b/tests/system/general/test_01_configuration_files.py index 22a62e189c6..03233a981c1 100644 --- a/tests/system/general/test_01_configuration_files.py +++ b/tests/system/general/test_01_configuration_files.py @@ -27,6 +27,7 @@ import os import time +from ansys.aedt.core import Circuit from ansys.aedt.core import Hfss3dLayout from ansys.aedt.core import Icepak from ansys.aedt.core import Q2d @@ -50,6 +51,7 @@ hfss3dl_existing_setup_proj_name = ( f"existing_hfss3dl_setup_v{config['desktopVersion'][-4:-2]}{config['desktopVersion'][-1:]}" ) +circuit_project_name = "differential_pairs.aedt" @pytest.fixture(scope="class") @@ -58,6 +60,12 @@ def aedtapp(add_app): return app +@pytest.fixture(scope="class") +def circuittest(add_app): + app = add_app(project_name=circuit_project_name, subfolder="T21", application=Circuit) + return app + + @pytest.fixture(scope="class") def q3dtest(add_app): app = add_app(project_name=q3d_file, application=Q3d, subfolder=test_subfolder) @@ -342,3 +350,10 @@ def test_05b_hfss3dlayout_existing_setup(self, hfss3dl_a, hfss3dl_b, local_scrat setup3 = hfss3dl_b.create_setup("My_HFSS_Setup_3") assert setup3.import_from_json(export_path) assert setup3.update() + + def test_06_circuit(self, circuittest, local_scratch): + path = circuittest.configurations.export_config() + assert os.path.exists(path) + circuittest.insert_design("new_import") + circuittest.configurations.import_config(path) + assert circuittest.configurations.export_config(os.path.join(local_scratch.path, "export_config.json")) diff --git a/tests/system/general/test_15_ibis_reader.py b/tests/system/general/test_15_ibis_reader.py index 8105d88f68e..470fb334803 100644 --- a/tests/system/general/test_15_ibis_reader.py +++ b/tests/system/general/test_15_ibis_reader.py @@ -102,3 +102,8 @@ def test_03_read_ibis_ami(self): os.path.join(TESTS_GENERAL_PATH, "example_models", test_subfolder, "ibis_ami_example_tx.ibs"), is_ami=True ) assert ibis_model.buffers["example_model_tx_ibis_ami_example_tx"].insert(0, 0) + assert ( + ibis_model.components["example_device_tx"] + .pins["14_example_device_tx_ibis_ami_example_tx_diff"] + .insert(0, 0.0512) + )