diff --git a/doc/changelog.d/6276.added.md b/doc/changelog.d/6276.added.md new file mode 100644 index 00000000000..0db9c71e2ed --- /dev/null +++ b/doc/changelog.d/6276.added.md @@ -0,0 +1 @@ +Create coil extension \ No newline at end of file diff --git a/doc/source/User_guide/extensions.rst b/doc/source/User_guide/extensions.rst index ddaa3a5f017..2412cdbe9ae 100644 --- a/doc/source/User_guide/extensions.rst +++ b/doc/source/User_guide/extensions.rst @@ -236,6 +236,13 @@ They are small automated workflows with a simple GUI. Export fields loss distribution to a generic format (CSV, TAB or NPY). + .. grid-item-card:: Vertical and flat coil geometries + :link: pyaedt_extensions_doc/maxwell/vertical_flat_coil + :link-type: doc + :margin: 2 2 0 0 + + Automation of vertical and flat coil geometries. + Templates ~~~~~~~~~ Templates to show how to build an extension consisting of a small automated workflow with a simple UI. @@ -259,6 +266,7 @@ Templates to show how to build an extension consisting of a small automated work pyaedt_extensions_doc/icepak/index pyaedt_extensions_doc/circuit/index pyaedt_extensions_doc/twinbuilder/index + pyaedt_extensions_doc/maxwell/index pyaedt_extensions_doc/templates/index diff --git a/doc/source/User_guide/pyaedt_extensions_doc/maxwell/index.rst b/doc/source/User_guide/pyaedt_extensions_doc/maxwell/index.rst index 180de3d0764..ad6387fdd33 100644 --- a/doc/source/User_guide/pyaedt_extensions_doc/maxwell/index.rst +++ b/doc/source/User_guide/pyaedt_extensions_doc/maxwell/index.rst @@ -8,4 +8,11 @@ Maxwell extensions :link-type: doc :margin: 2 2 0 0 - Predict and export fields distribution on a defined grid of points or on mesh nodes. \ No newline at end of file + Predict and export fields distribution on a defined grid of points or on mesh nodes. + + .. grid-item-card:: Vertical and flat coil geometries + :link: vertical_flat_coil + :link-type: doc + :margin: 2 2 0 0 + + Automation of vertical and flat coil geometries given specific parameters. \ No newline at end of file diff --git a/doc/source/User_guide/pyaedt_extensions_doc/maxwell/vertical_flat_coil.rst b/doc/source/User_guide/pyaedt_extensions_doc/maxwell/vertical_flat_coil.rst new file mode 100644 index 00000000000..7df4d1c5cf7 --- /dev/null +++ b/doc/source/User_guide/pyaedt_extensions_doc/maxwell/vertical_flat_coil.rst @@ -0,0 +1,29 @@ +Vertical and flat coil geometries +================================= + +This extension aims to create vertical or flat coil geometries in Maxwell 3D using specific parameters. +It supports more complex shapes than simple cylinders because it allows the segmentation of the coil profile as well as +the segmentation of corners. +The segmentation features are designed to optimize AEDT meshing operations. + +You can access the extension from the icon created on the **Automation** tab using the Extension Manager. + +The following image shows the extension user interface: + +.. image:: ../../../_static/extensions/vertical_flat_coil.png + :width: 600 + :alt: Vertical and Flat Coil UI + +The user can select the coil type by checking the **Vertical Coil** checkbox, define the coil parameters that are common +to both types, and then define the parameters that are specific to each type. Depending on the coil type (checkbox), +some entries are enabled or disabled. + +Finally, with one simple button click, the user can create the coil geometry in AEDT. + +You can also launch the extension user interface from the terminal. An example can be found here: + + +.. toctree:: + :maxdepth: 2 + + ../commandline \ No newline at end of file diff --git a/doc/source/_static/extensions/vertical_flat_coil.png b/doc/source/_static/extensions/vertical_flat_coil.png new file mode 100644 index 00000000000..a17c193663f Binary files /dev/null and b/doc/source/_static/extensions/vertical_flat_coil.png differ diff --git a/src/ansys/aedt/core/extensions/maxwell3d/images/large/coil.png b/src/ansys/aedt/core/extensions/maxwell3d/images/large/coil.png new file mode 100644 index 00000000000..d9807b927b8 Binary files /dev/null and b/src/ansys/aedt/core/extensions/maxwell3d/images/large/coil.png differ diff --git a/src/ansys/aedt/core/extensions/maxwell3d/toolkits_catalog.toml b/src/ansys/aedt/core/extensions/maxwell3d/toolkits_catalog.toml index 5c6e10458a9..5858de7074b 100644 --- a/src/ansys/aedt/core/extensions/maxwell3d/toolkits_catalog.toml +++ b/src/ansys/aedt/core/extensions/maxwell3d/toolkits_catalog.toml @@ -7,3 +7,9 @@ name = "Fields distribution" script = "fields_distribution.py" icon = "images/large/fields_distribution.png" template = "run_pyaedt_toolkit_script" + +[VerticalFlatCoil] +name = "Vertical flat coil design geometry" +script = "vertical_flat_coil.py" +icon = "images/large/coil.png" +template = "run_pyaedt_toolkit_script" diff --git a/src/ansys/aedt/core/extensions/maxwell3d/vertical_flat_coil.py b/src/ansys/aedt/core/extensions/maxwell3d/vertical_flat_coil.py new file mode 100644 index 00000000000..bbd5e3650c3 --- /dev/null +++ b/src/ansys/aedt/core/extensions/maxwell3d/vertical_flat_coil.py @@ -0,0 +1,481 @@ +# -*- 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 dataclasses import asdict +from dataclasses import dataclass +import os +import tkinter as tk +from tkinter import ttk + +import ansys.aedt.core +from ansys.aedt.core import get_pyaedt_app +import ansys.aedt.core.extensions +from ansys.aedt.core.extensions.misc import ExtensionCommon +from ansys.aedt.core.extensions.misc import ExtensionCommonData +from ansys.aedt.core.extensions.misc import get_aedt_version +from ansys.aedt.core.extensions.misc import get_arguments +from ansys.aedt.core.extensions.misc import get_port +from ansys.aedt.core.extensions.misc import get_process_id +from ansys.aedt.core.extensions.misc import is_student +from ansys.aedt.core.generic.numbers import Quantity +from ansys.aedt.core.internal.errors import AEDTRuntimeError +from ansys.aedt.core.modeler.advanced_cad.coil import Coil + +PORT = get_port() +VERSION = get_aedt_version() +AEDT_PROCESS_ID = get_process_id() +IS_STUDENT = is_student() +EXTENSION_TITLE = "Create vertical or flat coil" +EXTENSION_DEFAULT_ARGUMENTS = { + "is_vertical": False, + "name": "Coil", + "centre_x": "0mm", + "centre_y": "0mm", + "centre_z": "0mm", + "turns": "4", + "inner_width": "12mm", + "inner_length": "6mm", + "wire_radius": "1mm", + "inner_distance": "2mm", + "direction": "1", + "pitch": "3mm", + "arc_segmentation": "4", + "section_segmentation": "8", + "distance": "5mm", + "looping_position": "0.5", +} + + +@dataclass +class CoilExtensionData(ExtensionCommonData): + """Data class containing parameters to create vertical or flat coils.""" + + is_vertical: bool = EXTENSION_DEFAULT_ARGUMENTS["is_vertical"] + name: str = EXTENSION_DEFAULT_ARGUMENTS["name"] + centre_x: str = EXTENSION_DEFAULT_ARGUMENTS["centre_x"] + centre_y: str = EXTENSION_DEFAULT_ARGUMENTS["centre_y"] + centre_z: str = EXTENSION_DEFAULT_ARGUMENTS["centre_z"] + turns: str = EXTENSION_DEFAULT_ARGUMENTS["turns"] + inner_width: str = EXTENSION_DEFAULT_ARGUMENTS["inner_width"] + inner_length: str = EXTENSION_DEFAULT_ARGUMENTS["inner_length"] + wire_radius: str = EXTENSION_DEFAULT_ARGUMENTS["wire_radius"] + inner_distance: str = EXTENSION_DEFAULT_ARGUMENTS["inner_distance"] + direction: str = EXTENSION_DEFAULT_ARGUMENTS["direction"] + pitch: str = EXTENSION_DEFAULT_ARGUMENTS["pitch"] + arc_segmentation: str = EXTENSION_DEFAULT_ARGUMENTS["arc_segmentation"] + section_segmentation: str = EXTENSION_DEFAULT_ARGUMENTS["section_segmentation"] + distance: str = EXTENSION_DEFAULT_ARGUMENTS["distance"] + looping_position: str = EXTENSION_DEFAULT_ARGUMENTS["looping_position"] + + +class CoilExtension(ExtensionCommon): + """Extension to create vertical or flat coils in AEDT.""" + + def __init__(self, withdraw: bool = False): + # Initialize the common extension class with the title and theme color + super().__init__( + EXTENSION_TITLE, + theme_color="light", + withdraw=withdraw, + add_custom_content=False, + toggle_row=18, + toggle_column=2, + ) + # Tkinter widgets + self.check = False + self.name_text = None + self.x_pos_text = None + self.y_pos_text = None + self.turns_text = None + self.inner_width_text = None + self.inner_length_text = None + self.wire_radius_text = None + self.inner_distance_text = None + self.arc_segmentation_text = None + self.section_segmentation_text = None + self.looping_position_text = None + self.distance_text = None + self.z_pos_text = None + self.direction_text = None + self.pitch_text = None + # add custom content + self.add_extension_content() + + def add_extension_content(self): + """Add custom content to the extension UI.""" + self.root.geometry("850x750") + self.root.grid_rowconfigure(0, weight=1) + self.root.grid_columnconfigure(0, weight=1) + + def on_checkbox_toggle(): + if is_vertical.get() == 0: + # unchecked + # Flat coil + self.looping_position_text.config(state=tk.NORMAL) + self.distance_text.config(state=tk.NORMAL) + self.z_pos_text.config(state=tk.DISABLED) + self.direction_text.config(state=tk.DISABLED) + self.pitch_text.config(state=tk.DISABLED) + else: + # checked + # Vertical coil + self.looping_position_text.config(state=tk.DISABLED) + self.distance_text.config(state=tk.DISABLED) + self.z_pos_text.config(state=tk.NORMAL) + self.direction_text.config(state=tk.NORMAL) + self.pitch_text.config(state=tk.NORMAL) + + is_vertical_label = ttk.Label(self.root, text="Vertical Coil", style="PyAEDT.TLabel", width=20) + is_vertical_label.grid(row=0, column=0, pady=5, padx=5) + is_vertical = tk.IntVar(self.root, name="is_vertical") + self.check = ttk.Checkbutton( + self.root, variable=is_vertical, style="PyAEDT.TCheckbutton", name="is_vertical", command=on_checkbox_toggle + ) + self.check.grid(row=0, column=1, pady=5, padx=5) + is_vertical_description = tk.Text(self.root, width=40, height=2, name="is_vertical_description", wrap=tk.WORD) + is_vertical_description.insert("1.0", "If checkbox is selected, the coil is vertical, otherwise flat.") + is_vertical_description.grid(row=0, column=2, pady=1, padx=5) + is_vertical_description.config(state=tk.DISABLED) + + name = ttk.Label(self.root, text="Coil name:", style="PyAEDT.TLabel", width=20) + name.grid(row=1, column=0, pady=5, padx=5) + self.name_text = tk.Text(self.root, width=20, height=1, name="coil_name") + self.name_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.name_text.grid(row=1, column=1, pady=5, padx=5) + name_description = tk.Text(self.root, width=40, height=1, name="name_description", wrap=tk.WORD) + name_description.insert("1.0", "Coil name") + name_description.grid(row=1, column=2, pady=5, padx=5) + name_description.config(state=tk.DISABLED) + + centre_x = ttk.Label(self.root, text="x position:", style="PyAEDT.TLabel", width=20) + centre_x.grid(row=2, column=0, pady=5) + self.x_pos_text = tk.Text(self.root, width=20, height=1, name="centre_x") + self.x_pos_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.x_pos_text.grid(row=2, column=1, pady=5, padx=5) + x_pos_description = tk.Text(self.root, width=40, height=1, name="x_pos_description", wrap=tk.WORD) + x_pos_description.insert("1.0", "x position of coil center point") + x_pos_description.grid(row=2, column=2, pady=5, padx=5) + x_pos_description.config(state=tk.DISABLED) + + centre_y = ttk.Label(self.root, text="y position:", style="PyAEDT.TLabel", width=20) + centre_y.grid(row=3, column=0, pady=5) + self.y_pos_text = tk.Text(self.root, width=20, height=1, name="centre_y") + self.y_pos_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.y_pos_text.grid(row=3, column=1, pady=5, padx=5) + y_pos_description = tk.Text(self.root, width=40, height=1, name="y_pos_description", wrap=tk.WORD) + y_pos_description.insert("1.0", "y position of coil center point") + y_pos_description.grid(row=3, column=2, pady=5, padx=5) + y_pos_description.config(state=tk.DISABLED) + + turns = ttk.Label(self.root, text="Number of turns:", style="PyAEDT.TLabel", width=20) + turns.grid(row=4, column=0, pady=5) + self.turns_text = tk.Text(self.root, width=20, height=1, name="turns") + self.turns_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.turns_text.grid(row=4, column=1, pady=5, padx=5) + turns_description = tk.Text(self.root, width=40, height=1, name="turns_description", wrap=tk.WORD) + turns_description.insert("1.0", "Number of turns") + turns_description.grid(row=4, column=2, pady=5, padx=5) + turns_description.config(state=tk.DISABLED) + + inner_width = ttk.Label(self.root, text="Inner width:", style="PyAEDT.TLabel", width=20) + inner_width.grid(row=5, column=0, pady=5) + self.inner_width_text = tk.Text(self.root, width=20, height=1, name="inner_width") + self.inner_width_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.inner_width_text.grid(row=5, column=1, pady=5, padx=5) + inner_width_description = tk.Text(self.root, width=40, height=2, name="inner_width_description", wrap=tk.WORD) + inner_width_description.insert("1.0", "Inner width of the coil (length along X axis)") + inner_width_description.grid(row=5, column=2, pady=5, padx=5) + inner_width_description.config(state=tk.DISABLED) + + inner_length = ttk.Label(self.root, text="Inner length:", style="PyAEDT.TLabel", width=20) + inner_length.grid(row=6, column=0, pady=5) + self.inner_length_text = tk.Text(self.root, width=20, height=1, name="inner_length") + self.inner_length_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.inner_length_text.grid(row=6, column=1, pady=5, padx=5) + inner_length_description = tk.Text(self.root, width=40, height=2, name="inner_length_description", wrap=tk.WORD) + inner_length_description.insert("1.0", "Inner length of the coil (length along Y axis)") + inner_length_description.grid(row=6, column=2, pady=5, padx=5) + inner_length_description.config(state=tk.DISABLED) + + wire_radius = ttk.Label(self.root, text="Wire radius:", style="PyAEDT.TLabel", width=20) + wire_radius.grid(row=7, column=0, pady=5) + self.wire_radius_text = tk.Text(self.root, width=20, height=1, name="wire_radius") + self.wire_radius_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.wire_radius_text.grid(row=7, column=1, pady=5, padx=5) + wire_radius_description = tk.Text(self.root, width=40, height=2, name="wire_radius_description", wrap=tk.WORD) + wire_radius_description.insert("1.0", "Width of the wire (length along Y axis)") + wire_radius_description.grid(row=7, column=2, pady=5, padx=5) + wire_radius_description.config(state=tk.DISABLED) + + inner_distance = ttk.Label(self.root, text="Inner distance:", style="PyAEDT.TLabel", width=20) + inner_distance.grid(row=8, column=0, pady=5) + self.inner_distance_text = tk.Text(self.root, width=20, height=1, name="inner_distance") + self.inner_distance_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.inner_distance_text.grid(row=8, column=1, pady=5, padx=5) + inner_distance_description = tk.Text( + self.root, width=40, height=2, name="inner_distance_description", wrap=tk.WORD + ) + inner_distance_description.insert( + "1.0", "Distance between the coil and the inner rectangle (length along X or Y axis)" + ) + inner_distance_description.grid(row=8, column=2, pady=5, padx=5) + inner_distance_description.config(state=tk.DISABLED) + + arc_segmentation = ttk.Label(self.root, text="Arc segmentation:", style="PyAEDT.TLabel", width=20) + arc_segmentation.grid(row=9, column=0, pady=5) + self.arc_segmentation_text = tk.Text(self.root, width=20, height=1, name="arc_segmentation") + self.arc_segmentation_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.arc_segmentation_text.grid(row=9, column=1, pady=5, padx=5) + arc_segmentation_description = tk.Text( + self.root, width=40, height=2, name="arc_segmentation_description", wrap=tk.WORD + ) + arc_segmentation_description.insert("1.0", "number of segments into which to divide the coil corners") + arc_segmentation_description.grid(row=9, column=2, pady=5, padx=5) + arc_segmentation_description.config(state=tk.DISABLED) + + section_segmentation = ttk.Label(self.root, text="Section segmentation:", style="PyAEDT.TLabel", width=20) + section_segmentation.grid(row=10, column=0, pady=5) + self.section_segmentation_text = tk.Text(self.root, width=20, height=1, name="section_segmentation") + self.section_segmentation_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.section_segmentation_text.grid(row=10, column=1, pady=5, padx=5) + section_segmentation_description = tk.Text( + self.root, width=40, height=2, name="section_segmentation_description", wrap=tk.WORD + ) + section_segmentation_description.insert("1.0", "number of segments into which to divide the coil section") + section_segmentation_description.grid(row=10, column=2, pady=5, padx=5) + section_segmentation_description.config(state=tk.DISABLED) + + flat_specific_section = ttk.Label(self.root, text="Flat specific parameters:", style="PyAEDT.TLabel", width=20) + flat_specific_section.grid(row=11, column=1, pady=5, padx=5) + + looping_position = ttk.Label(self.root, text="Looping position:", style="PyAEDT.TLabel", width=20) + looping_position.grid(row=12, column=0, pady=5) + self.looping_position_text = tk.Text(self.root, width=20, height=1, name="looping_position") + self.looping_position_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.looping_position_text.grid(row=12, column=1, pady=5, padx=5) + looping_position_description = tk.Text( + self.root, width=40, height=1, name="looping_position_description", wrap=tk.WORD + ) + looping_position_description.insert("1.0", "Position of the loop, from 0.5 to 1") + looping_position_description.grid(row=12, column=2, pady=5, padx=5) + looping_position_description.config(state=tk.DISABLED) + + distance = ttk.Label(self.root, text="Distance:", style="PyAEDT.TLabel", width=20) + distance.grid(row=13, column=0, pady=5) + self.distance_text = tk.Text(self.root, width=20, height=1, name="distance") + self.distance_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.distance_text.grid(row=13, column=1, pady=5, padx=5) + distance_description = tk.Text(self.root, width=40, height=1, name="distance_description", wrap=tk.WORD) + distance_description.insert("1.0", "Distance between turns") + distance_description.grid(row=13, column=2, pady=5, padx=5) + distance_description.config(state=tk.DISABLED) + + vertical_specific_section = ttk.Label( + self.root, text="Vertical specific parameters:", style="PyAEDT.TLabel", width=25 + ) + vertical_specific_section.grid(row=14, column=1, pady=5, padx=5) + + centre_z = ttk.Label(self.root, text="z position:", style="PyAEDT.TLabel", width=20) + centre_z.grid(row=15, column=0, pady=5) + self.z_pos_text = tk.Text(self.root, width=20, height=1, name="centre_z", state=tk.DISABLED) + self.z_pos_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.z_pos_text.grid(row=15, column=1, pady=5, padx=5) + z_pos_description = tk.Text(self.root, width=40, height=1, name="z_pos_description", wrap=tk.WORD) + z_pos_description.insert("1.0", "z position of coil center point") + z_pos_description.grid(row=15, column=2, pady=5, padx=5) + z_pos_description.config(state=tk.DISABLED) + + direction = ttk.Label(self.root, text="Direction:", style="PyAEDT.TLabel", width=20) + direction.grid(row=16, column=0, pady=5) + self.direction_text = tk.Text(self.root, width=20, height=1, name="direction", state=tk.DISABLED) + self.direction_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.direction_text.grid(row=16, column=1, pady=5, padx=5) + direction_description = tk.Text(self.root, width=40, height=2, name="direction_description", wrap=tk.WORD) + direction_description.insert("1.0", "Direction of the coil (left side 1 or right -1)") + direction_description.grid(row=16, column=2, pady=5, padx=5) + direction_description.config(state=tk.DISABLED) + + pitch = ttk.Label(self.root, text="Pitch:", style="PyAEDT.TLabel", width=20) + pitch.grid(row=17, column=0, pady=5) + self.pitch_text = tk.Text(self.root, width=20, height=1, name="pitch", state=tk.DISABLED) + self.pitch_text.configure( + bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font + ) + self.pitch_text.grid(row=17, column=1, pady=5, padx=5) + pitch_description = tk.Text(self.root, width=40, height=2, name="pitch_description", wrap=tk.WORD) + pitch_description.insert("1.0", "Pitch of the coil (deviation along Z axis per turn)") + pitch_description.grid(row=17, column=2, pady=5, padx=5) + pitch_description.config(state=tk.DISABLED) + + def callback(extension: CoilExtension): + data = CoilExtensionData( + is_vertical=True if is_vertical.get() == 1 else False, + name=self.name_text.get("1.0", tk.END).strip(), + centre_x=self.x_pos_text.get("1.0", tk.END).strip(), + centre_y=self.y_pos_text.get("1.0", tk.END).strip(), + centre_z=self.z_pos_text.get("1.0", tk.END).strip(), + turns=self.turns_text.get("1.0", tk.END).strip(), + inner_width=self.inner_width_text.get("1.0", tk.END).strip(), + inner_length=self.inner_length_text.get("1.0", tk.END).strip(), + wire_radius=self.wire_radius_text.get("1.0", tk.END).strip(), + inner_distance=self.inner_distance_text.get("1.0", tk.END).strip(), + direction=self.direction_text.get("1.0", tk.END).strip(), + pitch=self.pitch_text.get("1.0", tk.END).strip(), + arc_segmentation=self.arc_segmentation_text.get("1.0", tk.END).strip(), + section_segmentation=self.section_segmentation_text.get("1.0", tk.END).strip(), + looping_position=self.looping_position_text.get("1.0", tk.END).strip(), + distance=self.distance_text.get("1.0", tk.END).strip(), + ) + extension.data = data + self.root.destroy() + + create_coil = ttk.Button( + self.root, + text="Create", + width=20, + style="PyAEDT.TButton", + name="create_coil", + command=lambda: callback(self), + ) + create_coil.grid(row=18, column=1, pady=5, padx=10) + + +def main(data: CoilExtensionData): + """Main function to create vertical or flat coils in AEDT.""" + app = ansys.aedt.core.Desktop( + new_desktop=False, + version=VERSION, + port=PORT, + aedt_process_id=AEDT_PROCESS_ID, + student_version=IS_STUDENT, + ) + + active_project = app.active_project() + active_design = app.active_design() + + project_name = active_project.GetName() + design_name = active_design.GetName() + + aedtapp = get_pyaedt_app(project_name, design_name) + if aedtapp.design_type != "Maxwell 3D": + raise AEDTRuntimeError("This extension can only be used with Maxwell 3D designs.") + + coil = Coil( + aedtapp, + name=data.name, + is_vertical=data.is_vertical, + centre_x=data.centre_x, + centre_y=data.centre_y, + centre_z=data.centre_z, + turns=data.turns, + inner_width=data.inner_width, + inner_length=data.inner_length, + wire_radius=data.wire_radius, + inner_distance=data.inner_distance, + direction=data.direction, + pitch=data.pitch, + arc_segmentation=data.arc_segmentation, + section_segmentation=data.section_segmentation, + looping_position=data.looping_position, + distance=data.distance, + ) + + data_dict = asdict(data) + + # Create polyline shape for coil + polyline = coil.create_vertical_path() if data.is_vertical else coil.create_flat_path() + + centre_x = Quantity(data_dict["centre_x"]).value + centre_y = Quantity(data_dict["centre_y"]).value + inner_y = Quantity(data_dict["inner_length"]).value + + if data.is_vertical: + centre_z = Quantity(data_dict["centre_z"]).value + inner_distance = Quantity(data_dict["inner_distance"]).value + pitch = Quantity(data_dict["pitch"]).value + turns = int(data_dict["turns"]) + start_point = [ + centre_x, + centre_y - 0.5 * inner_y - inner_distance, + centre_z + pitch * turns * 0.5, + ] + else: + inner_x = Quantity(data_dict["inner_width"]).value + start_position = Quantity(data_dict["looping_position"]).value + start_point = [ + centre_x + 0.25 * inner_x, + centre_y - (start_position - 0.5) * inner_y, + 0, + ] + # Create coil profile + coil.create_sweep_profile(start_point, polyline) + + if "PYTEST_CURRENT_TEST" not in os.environ: + extension.desktop.release_desktop(False, False) + return True + + +if __name__ == "__main__": # pragma: no cover + args = get_arguments(EXTENSION_DEFAULT_ARGUMENTS, EXTENSION_TITLE) + default_args = False + parameters = {} + # Open UI + if not args["is_batch"]: + extension: ExtensionCommon = CoilExtension(withdraw=False) + + tk.mainloop() + + main(extension.data) + else: + data = CoilExtensionData() + for key, value in args.items(): + setattr(data, key, value) + main(data) diff --git a/src/ansys/aedt/core/modeler/advanced_cad/coil.py b/src/ansys/aedt/core/modeler/advanced_cad/coil.py new file mode 100644 index 00000000000..0b9a8a4e36e --- /dev/null +++ b/src/ansys/aedt/core/modeler/advanced_cad/coil.py @@ -0,0 +1,386 @@ +# -*- 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 +from ansys.aedt.core.generic.numbers import Quantity +from ansys.aedt.core.modeler.cad.polylines import PolylineSegment + + +class Coil(object): + """Class to create vertical or flat coils in AEDT. + + Parameters + ---------- + name : str, optional + Name of the coil. The default is ``"Coil"``. + centre_x : str, optional + X coordinate of the coil center. The default is ``"0mm"``. + centre_y : str, optional + Y coordinate of the coil center. The default is ``"0mm"``. + centre_z : str, optional + Z coordinate of the coil center. The default is ``"0mm"``. + turns : str, optional + Number of turns in the coil. The default is ``"1"``. + inner_distance : str, optional + Distance between the coil and the inner rectangle (length along X or Y axis). + The default is ``"2mm"``. + inner_width : str, optional + Inner width of the coil (length along X axis). + The default is ``"12mm"``. + inner_length : str, optional + Inner height of the coil (length along Y axis). + The default is ``"6mm"``. + wire_radius : str, optional + Radius of the wire used in the coil. The default is ``"1mm"``. + distance : str, optional + Distance between the coil turns (length along X axis). + The default is ``"5mm"``. + looping_position : str, optional + Position of the loop, from 0.5 to 1. + The default is ``"0.5"``. + direction : str, optional + Direction of the coil winding. Use ``1`` for left side and ``-1`` otherwise. + The default is ``"1"``. + pitch : str, optional + Pitch of the coil in the vertical direction (length along Z axis). + The default is ``"3mm"``. + arc_segmentation : int, optional + Number of segments for the arcs in the coil path. + The default is ``1``. + section_segmentation : int, optional + Number of segments for the circular sections of the coil. + The default is ``1``. + """ + + def __init__( + self, + app, + name="Coil", + is_vertical=True, + centre_x="0mm", + centre_y="0mm", + centre_z="0mm", + turns="1", + inner_distance="2mm", + inner_width="12mm", + inner_length="6mm", + wire_radius="1mm", + distance="5mm", + looping_position="0.5", + direction="1", + pitch="3mm", + arc_segmentation=1, + section_segmentation=1, + ): + self._app = app + self.name = name + self.is_vertical = is_vertical + # Specific parameters + if not self.is_vertical: + # Flat coil + self.looping_position = Quantity(looping_position).value + self.distance = Quantity(distance).value + else: + # Vertical coil + self.centre_z = Quantity(centre_z).value + self.direction = int(direction) + self.pitch = Quantity(pitch).value + self.centre_x = Quantity(centre_x).value + self.centre_y = Quantity(centre_y).value + self.turns = int(turns) + self.inner_distance = Quantity(inner_distance).value + self.inner_width = Quantity(inner_width).value + self.inner_length = Quantity(inner_length).value + self.wire_radius = Quantity(wire_radius).value + self.arc_segmentation = int(arc_segmentation) + self.section_segmentation = int(section_segmentation) + self._app["arc_segmentation"] = 0 if int(self.arc_segmentation) == 1 else int(self.section_segmentation) + self._app["section_segmentation"] = 0 if int(self.section_segmentation) == 1 else int(self.section_segmentation) + self._app["wire_radius"] = Quantity(self.wire_radius, self._app.modeler.model_units) + + @pyaedt_function_handler() + def create_flat_path(self): + num_points = 13 * self.turns + 7 + + start_x = self.centre_x + 0.25 * self.inner_width + start_y = self.centre_y - (self.looping_position - 0.5) * self.inner_length + start_z = 0 + + points_x = [] + points_y = [] + points_z = [] + points = [] + + for i in range(0, num_points): + points_x.append(start_x) + points_y.append(start_y) + points_z.append(start_z) + + points_x[1] = self.centre_x + 0.5 * self.inner_width + points_y[1] = self.centre_y - (self.looping_position - 0.5) * self.inner_length + + points_x[2] = self.centre_x + 0.5 * self.inner_width # rotation + points_y[2] = self.centre_y + self.inner_distance - (self.looping_position - 0.5) * self.inner_length + + points_x[3] = self.centre_x + 0.5 * self.inner_width + self.inner_distance # dummy + points_y[3] = self.centre_y + 0.5 * self.inner_length + + for i in range(0, self.turns): + points_x[13 * i + 4] = self.centre_x + 0.5 * self.inner_width + self.inner_distance + i * self.distance + points_y[13 * i + 4] = self.centre_y + 0.5 * self.inner_length + i * self.distance + + points_x[13 * i + 5] = self.centre_x + 0.5 * self.inner_width + i * self.distance # rotation 1 + points_y[13 * i + 5] = self.centre_y + 0.5 * self.inner_length + i * self.distance + + points_x[13 * i + 6] = self.centre_x - 0.5 * self.inner_width - i * self.distance # dummy + points_y[13 * i + 6] = self.centre_y + 0.5 * self.inner_length + self.inner_distance + i * self.distance + + points_x[13 * i + 7] = self.centre_x - 0.5 * self.inner_width - i * self.distance + points_y[13 * i + 7] = self.centre_y + 0.5 * self.inner_length + self.inner_distance + i * self.distance + + points_x[13 * i + 8] = self.centre_x - 0.5 * self.inner_width - i * self.distance # rotation 2 + points_y[13 * i + 8] = self.centre_y + 0.5 * self.inner_length + i * self.distance + + points_x[13 * i + 9] = ( + self.centre_x - 0.5 * self.inner_width - self.inner_distance - i * self.distance + ) # dummy + points_y[13 * i + 9] = self.centre_y - 0.5 * self.inner_length - i * self.distance + + points_x[13 * i + 10] = self.centre_x - 0.5 * self.inner_width - self.inner_distance - i * self.distance + points_y[13 * i + 10] = self.centre_y - 0.5 * self.inner_length - i * self.distance + + points_x[13 * i + 11] = self.centre_x - 0.5 * self.inner_width - i * self.distance # rotation 3 + points_y[13 * i + 11] = self.centre_y - 0.5 * self.inner_length - i * self.distance + + points_x[13 * i + 12] = self.centre_x + 0.5 * self.inner_width + i * self.distance + points_y[13 * i + 12] = ( + self.centre_y - 0.5 * self.inner_length - self.inner_distance - i * self.distance + ) # dummy + + points_x[13 * i + 13] = self.centre_x + 0.5 * self.inner_width + (i + 1) * self.distance + points_y[13 * i + 13] = self.centre_y - 0.5 * self.inner_length - self.inner_distance - i * self.distance + + points_x[13 * i + 14] = self.centre_x + 0.5 * self.inner_width + (i + 1) * self.distance # rotation 4 + points_y[13 * i + 14] = self.centre_y - 0.5 * self.inner_length - i * self.distance + + points_x[13 * i + 15] = ( + self.centre_x + 0.5 * self.inner_width + (i + 1) * self.distance + self.inner_distance + ) # dummy + points_y[13 * i + 15] = self.centre_y - self.inner_distance + + if i < self.turns - 1: + points_x[13 * i + 16] = ( + self.centre_x + 0.5 * self.inner_width + self.inner_distance + (i + 1) * self.distance + ) + points_y[13 * i + 16] = self.centre_y - self.inner_distance + else: + points_x[13 * i + 16] = ( + self.centre_x + 0.5 * self.inner_width + self.inner_distance + (i + 1) * self.distance + ) + points_y[13 * i + 16] = ( + self.centre_y - self.inner_distance - (self.looping_position - 0.5) * self.inner_length + ) + + points_x[13 * (self.turns - 1) + 17] = ( + self.centre_x + 0.5 * self.inner_width + 2 * self.inner_distance + self.turns * self.distance + ) # rotation final + points_y[13 * (self.turns - 1) + 17] = ( + self.centre_y - self.inner_distance - (self.looping_position - 0.5) * self.inner_length + ) + + points_x[13 * (self.turns - 1) + 18] = ( + self.centre_x + 0.75 * self.inner_width + 2 * self.inner_distance + self.turns * self.distance + ) # dummy + points_y[13 * (self.turns - 1) + 18] = self.centre_y - (self.looping_position - 0.5) * self.inner_length + + points_x[13 * (self.turns - 1) + 19] = ( + self.centre_x + 0.75 * self.inner_width + 2 * self.inner_distance + self.turns * self.distance + ) + points_y[13 * (self.turns - 1) + 19] = self.centre_y - (self.looping_position - 0.5) * self.inner_length + + for i in range(num_points): + points.append([points_x[i], points_y[i], points_z[i]]) + + polyline_points = [points[0], points[1]] + segments_type = [ + PolylineSegment("Line"), + PolylineSegment( + "AngularArc", arc_center=points[2], arc_angle="90deg", arc_plane="XY", num_seg="arc_segmentation" + ), + ] + + for i in range(self.turns): + j = 1 if i != 0 else 0 + polyline_points.extend( + [ + points[13 * i + 3 + j], + points[13 * i + 7], + points[13 * i + 10], + points[13 * i + 13], + points[13 * i + 16], + ] + ) + for arc_index in [5, 8, 11, 14]: + segments_type.extend( + [ + PolylineSegment("Line"), + PolylineSegment( + "AngularArc", + arc_center=points[13 * i + arc_index], + arc_angle="90deg", + arc_plane="XY", + num_seg="arc_segmentation", + ), + ] + ) + segments_type.append(PolylineSegment("Line")) + + polyline_points.extend([points[13 * (self.turns - 1) + 16], points[13 * (self.turns - 1) + 19]]) + segments_type.extend( + [ + PolylineSegment("Line"), + PolylineSegment( + "AngularArc", + arc_center=points[13 * (self.turns - 1) + 17], + arc_angle="-90deg", + arc_plane="XY", + num_seg="arc_segmentation", + ), + PolylineSegment("Line"), + ] + ) + + polyline = self._app.modeler.create_polyline(points=polyline_points, segment_type=segments_type) + return polyline + + @pyaedt_function_handler() + def create_vertical_path(self): + num_points = 12 * self.turns + 2 + + start_x, start_y, start_z = ( + self.centre_x, + self.centre_y - 0.5 * self.inner_length - self.inner_distance, + self.centre_z + self.pitch * self.turns * 0.5, + ) + + points_x = [start_x] * num_points + points_y = [start_y] * num_points + points_z = [start_z] * num_points + points = [] + + for i in range(0, self.turns): + points_x[12 * i + 1] = self.centre_x + self.direction * 0.5 * self.inner_width + points_y[12 * i + 1] = self.centre_y - 0.5 * self.inner_length - self.inner_distance + points_z[12 * i + 1] = start_z - self.pitch * (0.25 + i) + + points_x[12 * i + 2] = self.centre_x + self.direction * 0.5 * self.inner_width # rotation + points_y[12 * i + 2] = self.centre_y - 0.5 * self.inner_length + points_z[12 * i + 2] = start_z - self.pitch * (0.25 + i) + + points_x[12 * i + 3] = ( + self.centre_x + self.direction * 0.5 * self.inner_width + self.direction * self.inner_distance + ) # dummy + points_y[12 * i + 3] = self.centre_y - 0.5 * self.inner_length + points_z[12 * i + 3] = start_z - self.pitch * (0.25 + i) + + points_x[12 * i + 4] = ( + self.centre_x + self.direction * 0.5 * self.inner_width + self.direction * self.inner_distance + ) + points_y[12 * i + 4] = self.centre_y + 0.5 * self.inner_length + points_z[12 * i + 4] = start_z - self.pitch * (0.25 + i) + + points_x[12 * i + 5] = self.centre_x + self.direction * 0.5 * self.inner_width # rotation 1 + points_y[12 * i + 5] = self.centre_y + 0.5 * self.inner_length + points_z[12 * i + 5] = start_z - self.pitch * (0.25 + i) + + points_x[12 * i + 6] = self.centre_x + self.direction * 0.5 * self.inner_width # dummy + points_y[12 * i + 6] = self.centre_y + 0.5 * self.inner_length + self.inner_distance + points_z[12 * i + 6] = start_z - self.pitch * (0.25 + i) + + points_x[12 * i + 7] = self.centre_x - self.direction * 0.5 * self.inner_width + points_y[12 * i + 7] = self.centre_y + 0.5 * self.inner_length + self.inner_distance + points_z[12 * i + 7] = start_z - self.pitch * (0.75 + i) + + points_x[12 * i + 8] = self.centre_x - self.direction * 0.5 * self.inner_width # rotation 2 + points_y[12 * i + 8] = self.centre_y + 0.5 * self.inner_length + points_z[12 * i + 8] = start_z - self.pitch * (0.75 + i) + + points_x[12 * i + 9] = ( + self.centre_x - self.direction * 0.5 * self.inner_width - self.direction * self.inner_distance + ) # dummy + points_y[12 * i + 9] = self.centre_y + 0.5 * self.inner_length + points_z[12 * i + 9] = start_z - self.pitch * (0.75 + i) + + points_x[12 * i + 10] = ( + self.centre_x - self.direction * 0.5 * self.inner_width - self.direction * self.inner_distance + ) + points_y[12 * i + 10] = self.centre_y - 0.5 * self.inner_length + points_z[12 * i + 10] = start_z - self.pitch * (0.75 + i) + + points_x[12 * i + 11] = self.centre_x - self.direction * 0.5 * self.inner_width # rotation 3 + points_y[12 * i + 11] = self.centre_y - 0.5 * self.inner_length + points_z[12 * i + 11] = start_z - self.pitch * (0.75 + i) + + points_x[12 * i + 12] = self.centre_x - self.direction * 0.5 * self.inner_width # dummy + points_y[12 * i + 12] = self.centre_y - 0.5 * self.inner_length - self.inner_distance + points_z[12 * i + 12] = start_z - self.pitch * (0.75 + i) + + points_x[12 * self.turns + 1] = self.centre_x + points_y[12 * self.turns + 1] = self.centre_y - 0.5 * self.inner_length - self.inner_distance + points_z[12 * self.turns + 1] = start_z - self.pitch * self.turns + + for i in range(num_points): + points.append([points_x[i], points_y[i], points_z[i]]) + + polyline_points = [] + segments_type = [] + + polyline_points.extend([points[0], points[1]]) + segments_type.extend([PolylineSegment("Line")]) + + for i in range(self.turns): + angle = "90deg" if self.direction == 1 else "-90deg" + for j in range(1, 13, 3): + polyline_points.extend([points[12 * i + j + 3]]) + segments_type.extend( + [ + PolylineSegment( + "AngularArc", + arc_center=points[12 * i + j + 1], + arc_angle=angle, + arc_plane="XY", + num_seg="arc_segmentation", + ), + PolylineSegment("Line"), + ] + ) + polyline = self._app.modeler.create_polyline(points=polyline_points, segment_type=segments_type, name=self.name) + return polyline + + @pyaedt_function_handler() + def create_sweep_profile(self, start_point, polyline): + profile = self._app.modeler.create_circle( + "YZ", start_point, "wire_radius", name=self.name, num_sides="section_segmentation" + ) + self._app.modeler.sweep_along_path(profile, sweep_object=polyline, draft_type="Extended") diff --git a/tests/system/extensions/test_move_it.py b/tests/system/extensions/test_move_it.py index 245d7016200..21bddf69b32 100644 --- a/tests/system/extensions/test_move_it.py +++ b/tests/system/extensions/test_move_it.py @@ -62,7 +62,7 @@ def test_move_it_generate_button(add_app): def test_move_it_exceptions(add_app): - """Test the Generate button in the Move IT extension.""" + """Test exceptions thrown by the Move IT extension.""" data = MoveItExtensionData(choice=None) with pytest.raises(AEDTRuntimeError): main(data) diff --git a/tests/system/extensions/test_vertical_flat_coil.py b/tests/system/extensions/test_vertical_flat_coil.py new file mode 100644 index 00000000000..f8e65f7ff1a --- /dev/null +++ b/tests/system/extensions/test_vertical_flat_coil.py @@ -0,0 +1,140 @@ +# -*- 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. + +import pytest + +from ansys.aedt.core import Maxwell3d +from ansys.aedt.core.extensions.maxwell3d.vertical_flat_coil import CoilExtension +from ansys.aedt.core.extensions.maxwell3d.vertical_flat_coil import CoilExtensionData +from ansys.aedt.core.extensions.maxwell3d.vertical_flat_coil import main + + +@pytest.fixture() +def m3d_app(add_app): + app = add_app(application=Maxwell3d) + yield app + app.close_project(app.project_name) + + +def test_vertical_flat_coil_create_button(m3d_app): + """Test the Create button in the Vertical and Flat coil extension.""" + + coil_data = CoilExtensionData( + is_vertical=False, + name="my_coil", + centre_x="0mm", + centre_y="0mm", + centre_z="", + turns="5", + inner_width="12mm", + inner_length="6mm", + wire_radius="1mm", + inner_distance="2mm", + direction="", + pitch="", + arc_segmentation="1", + section_segmentation="1", + distance="5mm", + looping_position="0.5", + ) + + extension = CoilExtension(withdraw=True) + extension.check.setvar("0") + extension.name_text.insert("1.0", coil_data.name) + extension.x_pos_text.insert("1.0", "0mm") + extension.y_pos_text.insert("1.0", "0mm") + extension.turns_text.insert("1.0", "5") + extension.inner_width_text.insert("1.0", "12mm") + extension.inner_length_text.insert("1.0", "6mm") + extension.wire_radius_text.insert("1.0", "1mm") + extension.inner_distance_text.insert("1.0", "2mm") + extension.arc_segmentation_text.insert("1.0", "1") + extension.section_segmentation_text.insert("1.0", "1") + extension.looping_position_text.insert("1.0", "0.5") + extension.distance_text.insert("1.0", "5mm") + extension.root.nametowidget("create_coil").invoke() + + assert coil_data == extension.data + assert main(extension.data) + assert len(m3d_app.modeler.solid_objects) == 1 + assert m3d_app.modeler.solid_objects[0].name == "my_coil" + assert {"arc_segmentation", "section_segmentation"}.issubset(m3d_app.variable_manager.design_variable_names) + + +def test_flat_coil_success(m3d_app): + """Test the Flat coil extension success.""" + + data = CoilExtensionData( + is_vertical=False, + name="my_coil", + centre_x="0mm", + centre_y="0mm", + centre_z="", + turns="5", + inner_width="12mm", + inner_length="6mm", + wire_radius="1mm", + inner_distance="2mm", + direction="", + pitch="", + arc_segmentation="1", + section_segmentation="1", + distance="5mm", + looping_position="0.5", + ) + assert main(data) + assert len(m3d_app.modeler.solid_objects) == 1 + assert m3d_app.modeler.solid_objects[0].name == "my_coil" + assert {"arc_segmentation", "section_segmentation"}.issubset(m3d_app.variable_manager.design_variable_names) + + +def test_vertical_coil_success(m3d_app): + """Test the Vertical coil extension success.""" + + data = CoilExtensionData( + is_vertical=True, + name="my_coil", + centre_x="0mm", + centre_y="0mm", + centre_z="0mm", + turns="5", + inner_width="12mm", + inner_length="6mm", + wire_radius="1mm", + inner_distance="2mm", + direction="1", + pitch="3mm", + arc_segmentation="1", + section_segmentation="1", + ) + assert main(data) + assert len(m3d_app.modeler.solid_objects) == 1 + assert {"arc_segmentation", "section_segmentation"}.issubset(m3d_app.variable_manager.design_variable_names) + + +def test_exception_invalid_data(m3d_app): + """Test exceptions thrown by the Vertical or Flat coil extension.""" + data = CoilExtensionData(centre_x="invalid") + with pytest.raises(ValueError): + main(data) diff --git a/tests/unit/extensions/test_move_it.py b/tests/unit/extensions/test_move_it.py index e30f1e9e7f6..ed6e87a3ef2 100644 --- a/tests/unit/extensions/test_move_it.py +++ b/tests/unit/extensions/test_move_it.py @@ -70,7 +70,7 @@ def test_move_it_extension_generate_button(mock_desktop, mock_aedt_app): """Test instantiation of the Move It extension.""" mock_desktop.return_value = MagicMock() - extension = MoveItExtension(withdraw=True) + extension = MoveItExtension(withdraw=False) extension.root.nametowidget("generate").invoke() data: MoveItExtensionData = extension.data diff --git a/tests/unit/extensions/test_vertical_flat_coil.py b/tests/unit/extensions/test_vertical_flat_coil.py new file mode 100644 index 00000000000..8bd2049b39f --- /dev/null +++ b/tests/unit/extensions/test_vertical_flat_coil.py @@ -0,0 +1,97 @@ +# -*- 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 unittest.mock import MagicMock +from unittest.mock import PropertyMock +from unittest.mock import patch + +import pytest + +from ansys.aedt.core.extensions.maxwell3d.vertical_flat_coil import EXTENSION_TITLE +from ansys.aedt.core.extensions.maxwell3d.vertical_flat_coil import CoilExtension +from ansys.aedt.core.extensions.misc import ExtensionCommon + + +@pytest.fixture +def mock_aedt_app(): + """Fixture to create a mock AEDT application.""" + mock_aedt_application = MagicMock() + mock_aedt_application.design_type = "Maxwell 3D" + + with patch.object(ExtensionCommon, "aedt_application", new_callable=PropertyMock) as mock_aedt_application_property: + mock_aedt_application_property.return_value = mock_aedt_application + yield mock_aedt_application + + +@patch("ansys.aedt.core.extensions.misc.Desktop", new_callable=PropertyMock) +def test_extension_default(mock_desktop, mock_aedt_app): + """Test instantiation of the Advanced Fields Calculator extension.""" + mock_desktop.return_value = MagicMock() + + extension = CoilExtension(withdraw=False) + + assert EXTENSION_TITLE == extension.root.title() + assert "light" == extension.root.theme + + extension.root.destroy() + + +@patch("ansys.aedt.core.extensions.misc.Desktop", new_callable=PropertyMock) +def test_create_button(mock_desktop, mock_aedt_app): + mock_desktop.return_value = MagicMock() + + extension = CoilExtension(withdraw=False) + + extension.root.nametowidget("create_coil").invoke() + data: CoilExtension = extension.data + + assert not data.is_vertical + assert getattr(data, "centre_x") == "" + assert getattr(data, "centre_y") == "" + assert getattr(data, "centre_z") == "" + assert getattr(data, "turns") == "" + assert getattr(data, "inner_width") == "" + assert getattr(data, "inner_length") == "" + assert getattr(data, "wire_radius") == "" + assert getattr(data, "inner_distance") == "" + assert getattr(data, "direction") == "" + assert getattr(data, "pitch") == "" + assert getattr(data, "arc_segmentation") == "" + assert getattr(data, "section_segmentation") == "" + assert getattr(data, "distance") == "" + assert getattr(data, "looping_position") == "" + + +@patch("ansys.aedt.core.extensions.misc.Desktop", new_callable=PropertyMock) +def test_is_vertical_checkbox(mock_desktop, mock_aedt_app): + """Test check and uncheck of the vertical coil checkbox.""" + mock_desktop.return_value = MagicMock() + + extension = CoilExtension(withdraw=False) + + # This toggle the checkbox + extension.root.nametowidget("is_vertical").invoke() + assert extension.root.getvar("is_vertical") == "1" + extension.root.nametowidget("is_vertical").invoke() + assert extension.root.getvar("is_vertical") == "0"