From 086817e0953dd91ce4f14f2f2a94c1b2f94fde63 Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 12:28:34 +0000 Subject: [PATCH 01/29] Added tests --- pyproject.toml | 8 +++ tests/test_design.py | 145 ++++++++++++++++++++++++++++++++++++++++++ tests/test_design2.py | 74 +++++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 tests/test_design.py create mode 100644 tests/test_design2.py diff --git a/pyproject.toml b/pyproject.toml index 9a8a1f5..b075f44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,11 +31,19 @@ dependencies=[ 'botorch', ] +[project.optional-dependencies] +dev = [ + 'pytest', +] + [tool.setuptools] packages = [ "machine_design", ] +[tool.pytest.ini_options] +testpaths = ["tests"] + [tool.ruff] line-length = 250 diff --git a/tests/test_design.py b/tests/test_design.py new file mode 100644 index 0000000..1932ec7 --- /dev/null +++ b/tests/test_design.py @@ -0,0 +1,145 @@ +import re + +import pytest + +from machine_design.design import Design + + +@pytest.fixture +def design(): + return Design(m2d=None) + + +def test_setup_name(design): + assert design.setup_name == "Setup1" + + +def test_iron(design): + assert design.Fe == "Cogent Power - M350-50A, B-H at 50Hz" + + +def test_geom_params(design): + assert design.geom_params == { + "DiaStatorGap": "79mm", + "DiaStatorYoke": "125mm", + "Airgap": "0.225mm", + "SlotNumber": "36", + "SlotType": "3", + "DiaShaft": "25mm", + "StackLength": "85mm", + } + + +def test_slot_params(design): + assert design.slot_params == { + "Hs0": "0.95mm", + "Hs1": "0.31mm", + "Hs2": "8.24mm", + "Bs0": "2.2mm", + "Bs1": "3.37mm", + "Bs2": "4.8mm", + "Rs": "1.5mm", + "SetAngle": "10deg", + } + + +def test_wind_params(design): + assert design.wind_params == { + "Layers": "1", + "ParallelPaths": "1", + "CoilPitch": "9", + "SlotLiner": "0.3mm", + "SpaceLayers": "0.2mm", + "Nc": "68", + } + + +def test_mod_params(design): + assert design.PolePairs == 2 + assert design.mod_params == { + "Poles": "2*2", + "ModelLength": "85mm", + "SymmetryFactor": "Poles", + "StatorSkewAngle": "0deg", + } + + +def test_oper_params(design): + assert design.oper_params == { + "Im": "1.5*sqrt(2)A", + "epsI": "pi/4", + "InitPos": "-30deg", + "f": "50Hz", + "RotSpeed": "1500.0rpm", + "Nper": "1/6", + "PointPer": "101", + } + + +def test_rot_points(design): + assert len(design.rot_points) == 6 + assert all(len(point) == 3 for point in design.rot_points) + + +def test_derived_params(design): + assert design.rotor_r_min == pytest.approx(12.5) + assert design.rotor_r_max == pytest.approx(39.5 - 0.225) + + +def test_solution_expressions(design): + assert design.solution_expressions == "Moving1.Torque" + + +def test_udp_par_list_stator(design): + keys = [item[0] for item in design.udp_par_list_stator] + assert keys == [ + "DiaGap", + "DiaYoke", + "Length", + "Skew", + "Slots", + "SlotType", + "Hs0", + "Hs01", + "Hs1", + "Hs2", + "Bs0", + "Bs1", + "Bs2", + "Rs", + "FilletType", + "HalfSlot", + "SegAngle", + "LenRegion", + "InfoCore", + ] + + +def test_output_vars_only_reference_earlier_keys(design): + # Ansys evaluates output variables in insertion order, so a formula referencing + # another output variable must not reference one that is defined later. + defined = set() + all_keys = set(design.output_vars.keys()) + for key, formula in design.output_vars.items(): + later_keys = all_keys - defined - {key} + referenced_later = [k for k in later_keys if re.search(rf"\b{re.escape(k)}\b", formula)] + assert not referenced_later, f"'{key}' formula references not-yet-defined {referenced_later}" + defined.add(key) + + +def test_output_vars_known_formula(design): + assert design.output_vars["Irms"] == "sqrt(I_d^2+I_q^2)/sqrt(2)" + + +def test_post_params_plot_names_unique(design): + plot_names = list(design.post_params.values()) + assert len(plot_names) == len(set(plot_names)) + + +def test_mm_to_str(design): + assert design.mm_to_str("geom_params", "DiaShaft") == pytest.approx(25.0) + + +def test_mm_to_str_raises_without_mm_suffix(design): + with pytest.raises(Exception): + design.mm_to_str("geom_params", "SlotNumber") diff --git a/tests/test_design2.py b/tests/test_design2.py new file mode 100644 index 0000000..1438684 --- /dev/null +++ b/tests/test_design2.py @@ -0,0 +1,74 @@ +import pytest + +from machine_design.design import Design +from machine_design.design2 import Design2 + + +@pytest.fixture +def base(): + return Design(m2d=None) + + +@pytest.fixture +def design2(): + return Design2(m2d=None) + + +def test_geom_params_overrides_slot_number_only(base, design2): + expected = dict(base.geom_params) + expected["SlotNumber"] = "40" + assert design2.geom_params == expected + + +def test_slot_params_overrides_bs1_bs2_setangle_only(base, design2): + expected = dict(base.slot_params) + expected["Bs1"] = "3.0mm" + expected["Bs2"] = "4.3mm" + expected["SetAngle"] = "9deg" + assert design2.slot_params == expected + + +def test_wind_params_overrides_nc_only(base, design2): + expected = dict(base.wind_params) + expected["Nc"] = "113" + assert design2.wind_params == expected + + +def test_oper_params_fully_replaced(design2): + assert design2.oper_params == { + "Id1": "0.0A", + "Iq1": "0.0A", + "Id3": "0.0A", + "Iq3": "0.0A", + "epsI1": "atan2(Iq1,Id1)", + "epsI3": "atan2(Iq3,Id3)", + "Im1": "sqrt(Id1^2+Iq1^2)", + "Im3": "sqrt(Id3^2+Iq3^2)", + "InitPos": "-45deg", + "f": "50Hz", + "RotSpeed": "1500.0rpm", + "Nper": "1/10", + "PointPer": "101", + } + + +def test_derived_params_are_not_set(design2): + # Design2.set_derived_params() is a no-op, unlike Design's. + assert not hasattr(design2, "rotor_r_min") + assert not hasattr(design2, "rotor_r_max") + + +def test_solution_expressions_is_a_list_ending_in_torque(design2): + assert isinstance(design2.solution_expressions, list) + assert design2.solution_expressions[-1] == "Moving1.Torque" + assert len(design2.solution_expressions) == 23 + + +def test_post_params_plot_names_unique(design2): + plot_names = list(design2.post_params.values()) + assert len(plot_names) == len(set(plot_names)) + + +def test_output_vars_known_values(design2): + assert design2.output_vars["Rstat"] == "19" + assert design2.output_vars["Lew"] == "0" From ea2f18dfad1889197ac622db00d8c8703fef3c6d Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 12:47:48 +0000 Subject: [PATCH 02/29] Added tests on Ansys --- pyproject.toml | 4 + tests/legacy/design.py | 567 +++++++++++++++++++++++++++++++++ tests/test_ansys_regression.py | 88 +++++ 3 files changed, 659 insertions(+) create mode 100644 tests/legacy/design.py create mode 100644 tests/test_ansys_regression.py diff --git a/pyproject.toml b/pyproject.toml index b075f44..ebdc752 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,10 @@ packages = [ [tool.pytest.ini_options] testpaths = ["tests"] +markers = [ + "ansys: requires a running Ansys Electronics Desktop session and a license seat; excluded by default", +] +addopts = "-m 'not ansys'" [tool.ruff] line-length = 250 diff --git a/tests/legacy/design.py b/tests/legacy/design.py new file mode 100644 index 0000000..bd0ba5d --- /dev/null +++ b/tests/legacy/design.py @@ -0,0 +1,567 @@ +# Frozen snapshot of machine_design/design.py at commit 086817e. +# Golden reference for tests/test_ansys_regression.py - do not update when design.py changes. +import numpy as np +from ansys.aedt.core import Desktop, Maxwell2d +from ansys.aedt.core.modeler.modeler_2d import Modeler2D + + +class Design: + def __init__(self, m2d: Maxwell2d) -> None: + self.set_parameters() + self.m2d = m2d + + @classmethod + def create(cls, project_name: str, design_name: str, file_name: str, **kwargs) -> "Design": + m2d = Maxwell2d(project=project_name, design=design_name, solution_type="TransientXY", **kwargs) + obj = cls(m2d) + obj.create_stator() + obj.save_project(file_name) + return obj + + @classmethod + def load(cls, file_name: str, **kwargs) -> "Design": + desktop = Desktop(**kwargs) + desktop.load_project(file_name) + + m2d = Maxwell2d() + m2d.set_active_design("Design01") + + return cls(m2d) + + def set_parameters(self) -> None: + self.setup_name = "Setup1" + self.set_iron() + self.set_geom_params() + self.set_slot_params() + self.set_winds_params() + self.set_mod_params() + self.set_oper_params() + self.set_rot_points() + self.set_derived_params() + self.set_solution_expressions() + self.set_udp_par_list_stator() + self.set_output_vars() + self.set_post_params() + + def set_iron(self): + self.Fe = "Cogent Power - M350-50A, B-H at 50Hz" + + def set_geom_params(self): + self.geom_params = { + "DiaStatorGap": "79mm", + "DiaStatorYoke": "125mm", + "Airgap": "0.225mm", + "SlotNumber": "36", + "SlotType": "3", + "DiaShaft": "25mm", + "StackLength": "85mm", + } + + def set_slot_params(self): + self.slot_params = { + "Hs0": "0.95mm", + "Hs1": "0.31mm", + "Hs2": "8.24mm", + "Bs0": "2.2mm", + "Bs1": "3.37mm", + "Bs2": "4.8mm", + "Rs": "1.5mm", + "SetAngle": "10deg", + } + + def set_winds_params(self): + self.wind_params = { + "Layers": "1", + "ParallelPaths": "1", + "CoilPitch": "9", # coil pitch in slots + "SlotLiner": "0.3mm", + "SpaceLayers": "0.2mm", + "Nc": "68", # turns per coil + } + + def set_mod_params(self): + self.PolePairs = 2 + self.mod_params = { + "Poles": f"2*{self.PolePairs}", + "ModelLength": "85mm", + "SymmetryFactor": "Poles", + "StatorSkewAngle": "0deg", + } + + def set_oper_params(self): + f = 50 # [Hz] + RotSpeed = 60 * f / self.PolePairs # [rpm] + self.oper_params = { + "Im": "1.5*sqrt(2)A", + "epsI": "pi/4", # current angle + "InitPos": "-30deg", + "f": f"{f}Hz", + "RotSpeed": f"{RotSpeed}rpm", + "Nper": "1/6", # number of included periods + "PointPer": "101", # number of time points per period + } + + def set_rot_points(self): + self.rot_points = [ + ["DiaShaft/2*cos(360deg/SymmetryFactor)", "DiaShaft/2*sin(360deg/SymmetryFactor)", "0mm"], + ["DiaShaft/2*cos(360deg/(2*SymmetryFactor))", "DiaShaft/2*sin(360deg/(2*SymmetryFactor))", "0mm"], + ["DiaShaft/2", "0mm", "0mm"], + ["DiaStatorGap/2-Airgap", "0mm", "0mm"], + ["(DiaStatorGap/2-Airgap)*cos(360deg/(2*SymmetryFactor))", "(DiaStatorGap/2-Airgap)*sin(360deg/(2*SymmetryFactor))", "0mm"], + ["(DiaStatorGap/2-Airgap)*cos(360deg/SymmetryFactor)", "(DiaStatorGap/2-Airgap)*sin(360deg/SymmetryFactor)", "0mm"], + ] + + def set_derived_params(self): + self.rotor_r_min = self.mm_to_str("geom_params", "DiaShaft") / 2 + self.rotor_r_max = self.mm_to_str("geom_params", "DiaStatorGap") / 2 - self.mm_to_str("geom_params", "Airgap") + + def set_solution_expressions(self): + self.solution_expressions = "Moving1.Torque" + + def set_udp_par_list_stator(self): + self.udp_par_list_stator = [ + ["DiaGap", "DiaStatorGap"], + ["DiaYoke", "DiaStatorYoke"], + ["Length", "0mm"], + ["Skew", "0deg"], + ["Slots", "SlotNumber"], + ["SlotType", "SlotType"], + ["Hs0", "Hs0"], + ["Hs01", "0mm"], + ["Hs1", "Hs1"], + ["Hs2", "Hs2"], + ["Bs0", "Bs0"], + ["Bs1", "Bs1"], + ["Bs2", "Bs2"], + ["Rs", "Rs"], + ["FilletType", "0"], + ["HalfSlot", "0"], + ["SegAngle", "0deg"], + ["LenRegion", "0mm"], + ["InfoCore", "0"], + ] + + def set_output_vars(self): + self.output_vars = { + "pos": "(Moving1.Position -InitPos) * Poles/2", + "cos0": "cos(pos)", + "cos1": "cos(pos-2*PI/3)", + "cos2": "cos(pos-4*PI/3)", + "sin0": "sin(pos)", + "sin1": "sin(pos-2*PI/3)", + "sin2": "sin(pos-4*PI/3)", + "Lad": "L(PhaseA,PhaseA)*cos0 + L(PhaseA,PhaseB)*cos1 + L(PhaseA,PhaseC)*cos2", + "Laq": "L(PhaseA,PhaseA)*sin0 + L(PhaseA,PhaseB)*sin1 + L(PhaseA,PhaseC)*sin2", + "Lbd": "L(PhaseB,PhaseA)*cos0 + L(PhaseB,PhaseB)*cos1 + L(PhaseB,PhaseC)*cos2", + "Lbq": "L(PhaseB,PhaseA)*sin0 + L(PhaseB,PhaseB)*sin1 + L(PhaseB,PhaseC)*sin2", + "Lcd": "L(PhaseC,PhaseA)*cos0 + L(PhaseC,PhaseB)*cos1 + L(PhaseC,PhaseC)*cos2", + "Lcq": "L(PhaseC,PhaseA)*sin0 + L(PhaseC,PhaseB)*sin1 + L(PhaseC,PhaseC)*sin2", + "L_d": "(Lad*cos0 + Lbd*cos1 + Lcd*cos2) * 2/3", + "L_q": "(Laq*sin0 + Lbq*sin1 + Lcq*sin2) * 2/3", + "Flux_d": "(FluxLinkage(PhaseA)*cos0+FluxLinkage(PhaseB)*cos1+FluxLinkage(PhaseC)*cos2)*2/3", + "Flux_q": "-(FluxLinkage(PhaseA)*sin0+FluxLinkage(PhaseB)*sin1+FluxLinkage(PhaseC)*sin2)*2/3", + "Ui_d": "(InducedVoltage(PhaseA)*cos0+InducedVoltage(PhaseB)*cos1+InducedVoltage(PhaseC)*cos2)*2/3", + "Ui_q": "-(InducedVoltage(PhaseA)*sin0+InducedVoltage(PhaseB)*sin1+InducedVoltage(PhaseC)*sin2)*2/3", + "I_d": "(InputCurrent(PhaseA)*cos0 + InputCurrent(PhaseB)*cos1 + InputCurrent(PhaseC)*cos2)*2/3", + "I_q": "-(InputCurrent(PhaseA)*sin0 + InputCurrent(PhaseB)*sin1 + InputCurrent(PhaseC)*sin2)*2/3", + "Irms": "sqrt(I_d^2+I_q^2)/sqrt(2)", + } + + def set_post_params(self): + self.post_params = { # reports + ("InducedVoltage(PhaseA)", "InducedVoltage(PhaseB)", "InducedVoltage(PhaseC)"): "InducedVoltage", + ("Moving1.Torque"): "Torque", + ("InputCurrent(PhaseA)", "InputCurrent(PhaseB)", "InputCurrent(PhaseC)"): "Current", + ( + "FluxLinkage(PhaseA)", + "FluxLinkage(PhaseB)", + "FluxLinkage(PhaseC)", + ): "FluxLinkage", + ("I_d", "I_q"): "Current_dq", + ("Flux_d", "Flux_q"): "FluxLinkage_dq", + ("Ui_d", "Ui_q"): "InducedVoltage_dq", + ("L_d", "L_q"): "Inductance_dq", + } + + def create_stator(self) -> None: + m2d = self.m2d + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + # Define design variables from the created dictionaries. + modeler.model_units = "mm" + for k, v in self.geom_params.items(): + m2d[k] = v + for k, v in self.wind_params.items(): + m2d[k] = v + for k, v in self.slot_params.items(): + m2d[k] = v + for k, v in self.mod_params.items(): + m2d[k] = v + for k, v in self.oper_params.items(): + m2d[k] = v + + # VACUUM OBJECTS + # Outer region + region_id = modeler.create_circle( + origin=[0, 0, 0], + radius="DiaStatorYoke/2", + # num_sides="SegAngle", + is_covered=True, + name="Region", + ) + # Band + band_id = modeler.create_circle( + origin=[0, 0, 0], + radius="(DiaStatorGap - (1.0 * Airgap))/2", + # num_sides="mapping_angle", + is_covered=True, + name="Band", + ) + # Shaft + shaft_id = modeler.create_circle( + origin=[0, 0, 0], + radius="DiaShaft/2", + is_covered=True, + name="Shaft", + ) + + # motion setup + m2d.assign_rotate_motion( + assignment="Band", + coordinate_system="Global", + axis="Z", + positive_movement=True, + start_position="InitPos", + angular_velocity="RotSpeed", + has_rotation_limits=False, + ) + + # Together + vacuum_obj_id = [ + shaft_id, + region_id, + band_id, + ] # put shaft first + for item in vacuum_obj_id: + item.color = (0, 255, 255) + item.transparency = 0.95 + + # Fit all view + modeler.fit_all() + + # Stator geometry + stator_id = modeler.create_udp( + dll="RMxprt/SlotCore.dll", + parameters=self.udp_par_list_stator, + library="syslib", + name="Stator", + # SolveInside="True", + ) + # Stator properties + stator_id.material_name = self.Fe + stator_id.color = (192, 192, 192) # rgb + stator_id.transparency = 0.0 + + # Winding + coil_id = modeler.create_rectangle( + origin=["DiaStatorGap/2+Hs0+Hs1+SlotLiner", "-(Bs1/2-SlotLiner)", 0], + sizes=["(Hs2+Rs/2-2*SlotLiner)/Layers-(Layers-1)*SpaceLayers/2", "Bs1-2*SlotLiner", 0], + name="Coil", + material="copper", + ) + coil_id.color = (255, 128, 0) + coil_id.transparency = 0.0 + modeler.rotate(assignment=coil_id, axis="Z", angle="360deg/SlotNumber/2") + coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", clones="SlotNumber/Poles", create_new_objects=True) + id_coils = modeler.get_objects_w_string(string_name="Coil", case_sensitive=True) + + # Create section of machine + object_list = [stator_id] + vacuum_obj_id + modeler.create_coordinate_system( + origin=[0, 0, 0], + reference_cs="Global", + name="Section", + mode="axis", + x_pointing=["cos(360deg/SymmetryFactor)", "sin(360deg/SymmetryFactor)", 0], + y_pointing=["-sin(360deg/SymmetryFactor)", "cos(360deg/SymmetryFactor)", 0], + ) + modeler.set_working_coordinate_system("Section") + modeler.split(assignment=object_list, plane="ZX", sides="NegativeOnly") + modeler.set_working_coordinate_system("Global") + modeler.split(assignment=object_list, plane="ZX", sides="PositiveOnly") + + # Symmetrical boundary conditions + pos_1 = "DiaStatorGap/4" + id_bc_1 = modeler.get_edgeid_from_position(position=[pos_1, 0, 0], assignment="Region") + id_bc_2 = modeler.get_edgeid_from_position( + position=[ + pos_1 + "*cos((360deg/SymmetryFactor))", + pos_1 + "*sin((360deg/SymmetryFactor))", + 0, + ], + assignment="Region", + ) + m2d.assign_master_slave( + independent=id_bc_1, + dependent=id_bc_2, + reverse_master=True, + reverse_slave=False, + same_as_master=False, + boundary="Symmetry", + ) + # Zero vector potential + pos_2 = "(DiaStatorYoke/2)" + id_bc_az = modeler.get_edgeid_from_position( + position=[ + pos_2 + "*cos((360deg/SymmetryFactor/2))", + pos_2 + "*sin((360deg/SymmetryFactor)/2)", + 0, + ], + assignment="Region", + ) + m2d.assign_vector_potential(assignment=id_bc_az, vector_value=0, boundary="A0") + + self.assign_stator_coils() + + # Mesh operation + m2d.mesh.assign_length_mesh( + assignment=id_coils, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="coils", + ) + m2d.mesh.assign_length_mesh( + assignment=stator_id, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="stator", + ) + + # core loss + m2d.set_core_losses("Stator", core_loss_on_field=False) + + # inductance calculation + self.inductance_computation() + + # model depth + m2d.model_depth = "StackLength" + # symmetry + m2d.change_symmetry_multiplier("SymmetryFactor") + # Calculation setup + setup = m2d.create_setup(name=self.setup_name) + setup.props["StopTime"] = "Nper/f" + setup.props["TimeStep"] = "1/(f*(PointPer-1))" + setup.props["SaveFieldsType"] = "None" + setup.props["OutputPerObjectCoreLoss"] = False + setup.props["OutputPerObjectSolidLoss"] = True + setup.props["OutputError"] = True + setup.update() + m2d.validate_simple() + + for k, v in self.output_vars.items(): + m2d.create_output_variable(k, v) + + for k, v in self.post_params.items(): + expressions = list(k) if isinstance(k, tuple) else [k] # if multiple report, use list(k). Else, use k + m2d.post.create_report( + expressions=expressions, + setup_sweep_name="", + domain="Sweep", + variations=None, + primary_sweep_variable="Time", + secondary_sweep_variable=None, + report_category=None, + plot_type="Rectangular Plot", + context=None, + subdesign_id=None, + polyline_points=1001, + plot_name=v, + ) + + def assign_stator_coils(self): + m2d = self.m2d + + # Excitations + I_A = "Im * cos(2*pi*f*time+epsI)" + I_B = "Im * cos(2*pi*f*time-120deg+epsI)" + I_C = "Im * cos(2*pi*f*time-240deg+epsI)" + + # Define phase windings + m2d.assign_coil( + assignment=["Coil"], + conductors_number="Nc", + polarity="Positive", + name="CS1", + ) + m2d.assign_coil( + assignment=["Coil_1"], + conductors_number="Nc", + polarity="Positive", + name="CS2", + ) + m2d.assign_coil( + assignment=["Coil_2"], + conductors_number="Nc", + polarity="Positive", + name="CS3", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_A, + parallel_branches="ParallelPaths", + name="PhaseA", + ) + m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS2", "CS3"]) + m2d.assign_coil( + assignment=["Coil_6"], + conductors_number="Nc", + polarity="Positive", + name="CS7", + ) + m2d.assign_coil( + assignment=["Coil_7"], + conductors_number="Nc", + polarity="Positive", + name="CS8", + ) + m2d.assign_coil( + assignment=["Coil_8"], + conductors_number="Nc", + polarity="Positive", + name="CS9", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_B, + parallel_branches="ParallelPaths", + name="PhaseB", + ) + m2d.add_winding_coils(assignment="PhaseB", coils=["CS7", "CS8", "CS9"]) + m2d.assign_coil( + assignment=["Coil_3"], + conductors_number="Nc", + polarity="Negative", + name="CS4", + ) + m2d.assign_coil( + assignment=["Coil_4"], + conductors_number="Nc", + polarity="Negative", + name="CS5", + ) + m2d.assign_coil( + assignment=["Coil_5"], + conductors_number="Nc", + polarity="Negative", + name="CS6", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_C, + parallel_branches="ParallelPaths", + name="PhaseC", + ) + m2d.add_winding_coils(assignment="PhaseC", coils=["CS4", "CS5", "CS6"]) + + def inductance_computation(self): + self.m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=False) + + def add_rotor(self) -> None: + modeler = self.m2d.modeler + assert isinstance(modeler, Modeler2D) + + rotor_id = modeler.create_polyline(points=self.rot_points, segment_type=["Arc", "Line", "Arc"], cover_surface=True, name="Rotor") + self.rotor_id = rotor_id + rotor_id.material_name = self.Fe + rotor_id.color = (192, 192, 192) # rgb + rotor_id.transparency = 0.0 + + def add_rotor_barrier(self, barrier_points, segment_type=None) -> None: + modeler = self.m2d.modeler + assert isinstance(modeler, Modeler2D) + + # Round it for Ansys + barrier_points = np.round(barrier_points, 6) + + # Potentially add the z axis + if barrier_points.shape[1] == 2: + barrier_points = np.hstack((barrier_points, np.zeros((len(barrier_points), 1)))) + + # Convert them into a string format and interpolate + points_str = [[str(y) for y in x] for x in barrier_points] + barrier_id = modeler.create_polyline(points=points_str, segment_type=segment_type, cover_surface=True, name="Barrier") + + # Remove the barrier + self.rotor_id.subtract(barrier_id) + modeler.delete(barrier_id) + + # TODO: change the other arguments to kwargs + def compute(self, *args, NUM_CORES: int = 1): + m2d = self.m2d + assert m2d.mesh is not None + assert m2d.post is not None + + m2d.mesh.assign_length_mesh( + assignment=self.rotor_id, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="rotor", + ) + # core loss rotor + m2d.set_core_losses("Rotor", core_loss_on_field=False) + + self.set_variables(*args) + + # Analyze + m2d.analyze_setup(self.setup_name, use_auto_settings=False, cores=NUM_CORES) + + solutions = m2d.post.get_solution_data(expressions=self.solution_expressions, primary_sweep_variable="Time") + try: + result = self.extract_results(solutions) + except AttributeError: + result = None + + # Delete solution data to prevent the saving size to explode + self.m2d.odesign.DeleteFullVariation("All", False) + + return result + + def set_variables(self, *args): + pass + + def extract_results(self, solutions): + return solutions.data_magnitude() + + def delete_rotor(self) -> None: + assert isinstance(self.m2d.modeler, Modeler2D) + self.m2d.modeler.delete(self.rotor_id) + + def save_design(self, file_name: str, **kwargs) -> None: + show = kwargs.pop("show", False) + view = kwargs.pop("view", "xy") + self.m2d.plot(show=show, output_file=file_name, view=view) + + def save_project(self, file_name: str | None = None) -> None: + if file_name is None: + self.m2d.save_project() + else: + self.m2d.save_project(file_name) + + def close_project(self) -> None: + self.m2d.close_desktop() + + def mm_to_str(self, var, field) -> float: + val = getattr(self, var)[field] + if not val.endswith("mm"): + raise Exception("val must end with mm") + return float(val[:-2]) diff --git a/tests/test_ansys_regression.py b/tests/test_ansys_regression.py new file mode 100644 index 0000000..fb34003 --- /dev/null +++ b/tests/test_ansys_regression.py @@ -0,0 +1,88 @@ +"""Golden-master check: tests/legacy/design.py vs. the live machine_design/design.py. + +Generates one feasible rotor design with HacklGenerator_OneLambda, builds it with both +the frozen reference implementation and the current implementation, runs both through +Ansys, and asserts the resulting torque waveforms match. Run explicitly with: + + pytest -m ansys tests/test_ansys_regression.py + +Requires a running Ansys Electronics Desktop session and a free license seat. +""" + +import importlib.util +from pathlib import Path + +import numpy as np +import pytest + +from machine_design.design import Design as LiveDesign +from machine_design.generators import HacklGenerator_OneLambda + +pytestmark = pytest.mark.ansys + +AEDT_VERSION = "2024.1" +R_STATOR_END = 0.7 +OFFSET = 0.35 +SEED = 42 +NUM_CORES = 1 + + +def _load_legacy_design_class(): + legacy_path = Path(__file__).parent / "legacy" / "design.py" + spec = importlib.util.spec_from_file_location("legacy_design", legacy_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.Design + + +def _generate_one_lambda_barriers(): + # rotor_r_min/rotor_r_max are pure-Python derived params, so a throwaway + # instance (no Ansys) is enough to size the generator. + dummy = LiveDesign(m2d=None) + generator = HacklGenerator_OneLambda(dummy, R_STATOR_END, offset=OFFSET) + + np.random.seed(SEED) + while True: + params = generator.random_parameters() + generator.set_parameters(params) + barriers = generator.generate_barriers() + barriers = generator.split_barriers(barriers) + if generator.feasible_barriers(barriers): + return barriers + + +def _build_and_compute(design_cls, project_name, file_name, barriers): + design = design_cls.create( + project_name, + "Design01", + file_name, + version=AEDT_VERSION, + non_graphical=True, + new_desktop=False, + close_on_exit=False, + ) + design.add_rotor() + for barrier in barriers: + design.add_rotor_barrier(barrier) + torque = design.compute(NUM_CORES=NUM_CORES) + design.delete_rotor() + return design, torque + + +def test_legacy_and_live_design_produce_same_torque(tmp_path): + barriers = _generate_one_lambda_barriers() + LegacyDesign = _load_legacy_design_class() + + legacy_design, live_design = None, None + try: + legacy_design, torque_legacy = _build_and_compute(LegacyDesign, "RegressionTest_legacy", str(tmp_path / "legacy.aedt"), barriers) + live_design, torque_live = _build_and_compute(LiveDesign, "RegressionTest_live", str(tmp_path / "live.aedt"), barriers) + + assert torque_legacy is not None + assert torque_live is not None + np.testing.assert_allclose(torque_live, torque_legacy, rtol=1e-6) + finally: + if live_design is not None: + live_design.close_project() + elif legacy_design is not None: + legacy_design.close_project() From c50bbd07648cf32c12d96e2531a94a836c42057a Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 13:07:00 +0000 Subject: [PATCH 03/29] Refactor --- machine_design/design.py | 582 ++++----------------------- machine_design/design_computation.py | 281 +++++++++++++ machine_design/design_geometry.py | 318 +++++++++++++++ 3 files changed, 673 insertions(+), 508 deletions(-) create mode 100644 machine_design/design_computation.py create mode 100644 machine_design/design_geometry.py diff --git a/machine_design/design.py b/machine_design/design.py index 3324a50..5c0c0e7 100644 --- a/machine_design/design.py +++ b/machine_design/design.py @@ -1,12 +1,14 @@ -import numpy as np from ansys.aedt.core import Desktop, Maxwell2d -from ansys.aedt.core.modeler.modeler_2d import Modeler2D + +from .design_computation import Computation, ComputationBase +from .design_geometry import Geometry, GeometryBase class Design: - def __init__(self, m2d: Maxwell2d) -> None: - self.set_parameters() + def __init__(self, m2d: Maxwell2d, geometry: GeometryBase | None = None, computation: ComputationBase | None = None) -> None: self.m2d = m2d + self.geometry = geometry if geometry is not None else Geometry() + self.computation = computation if computation is not None else Computation(self.geometry) @classmethod def create(cls, project_name: str, design_name: str, file_name: str, **kwargs) -> "Design": @@ -26,523 +28,93 @@ def load(cls, file_name: str, **kwargs) -> "Design": return cls(m2d) - def set_parameters(self) -> None: - self.setup_name = "Setup1" - self.set_iron() - self.set_geom_params() - self.set_slot_params() - self.set_winds_params() - self.set_mod_params() - self.set_oper_params() - self.set_rot_points() - self.set_derived_params() - self.set_solution_expressions() - self.set_udp_par_list_stator() - self.set_output_vars() - self.set_post_params() - - def set_iron(self): - self.Fe = "Cogent Power - M350-50A, B-H at 50Hz" - - def set_geom_params(self): - self.geom_params = { - "DiaStatorGap": "79mm", - "DiaStatorYoke": "125mm", - "Airgap": "0.225mm", - "SlotNumber": "36", - "SlotType": "3", - "DiaShaft": "25mm", - "StackLength": "85mm", - } - - def set_slot_params(self): - self.slot_params = { - "Hs0": "0.95mm", - "Hs1": "0.31mm", - "Hs2": "8.24mm", - "Bs0": "2.2mm", - "Bs1": "3.37mm", - "Bs2": "4.8mm", - "Rs": "1.5mm", - "SetAngle": "10deg", - } - - def set_winds_params(self): - self.wind_params = { - "Layers": "1", - "ParallelPaths": "1", - "CoilPitch": "9", # coil pitch in slots - "SlotLiner": "0.3mm", - "SpaceLayers": "0.2mm", - "Nc": "68", # turns per coil - } - - def set_mod_params(self): - self.PolePairs = 2 - self.mod_params = { - "Poles": f"2*{self.PolePairs}", - "ModelLength": "85mm", - "SymmetryFactor": "Poles", - "StatorSkewAngle": "0deg", - } - - def set_oper_params(self): - f = 50 # [Hz] - RotSpeed = 60 * f / self.PolePairs # [rpm] - self.oper_params = { - "Im": "1.5*sqrt(2)A", - "epsI": "pi/4", # current angle - "InitPos": "-30deg", - "f": f"{f}Hz", - "RotSpeed": f"{RotSpeed}rpm", - "Nper": "1/6", # number of included periods - "PointPer": "101", # number of time points per period - } - - def set_rot_points(self): - self.rot_points = [ - ["DiaShaft/2*cos(360deg/SymmetryFactor)", "DiaShaft/2*sin(360deg/SymmetryFactor)", "0mm"], - ["DiaShaft/2*cos(360deg/(2*SymmetryFactor))", "DiaShaft/2*sin(360deg/(2*SymmetryFactor))", "0mm"], - ["DiaShaft/2", "0mm", "0mm"], - ["DiaStatorGap/2-Airgap", "0mm", "0mm"], - ["(DiaStatorGap/2-Airgap)*cos(360deg/(2*SymmetryFactor))", "(DiaStatorGap/2-Airgap)*sin(360deg/(2*SymmetryFactor))", "0mm"], - ["(DiaStatorGap/2-Airgap)*cos(360deg/SymmetryFactor)", "(DiaStatorGap/2-Airgap)*sin(360deg/SymmetryFactor)", "0mm"], - ] - - def set_derived_params(self): - self.rotor_r_min = self.mm_to_str("geom_params", "DiaShaft") / 2 - self.rotor_r_max = self.mm_to_str("geom_params", "DiaStatorGap") / 2 - self.mm_to_str("geom_params", "Airgap") - - def set_solution_expressions(self): - self.solution_expressions = "Moving1.Torque" - - def set_udp_par_list_stator(self): - self.udp_par_list_stator = [ - ["DiaGap", "DiaStatorGap"], - ["DiaYoke", "DiaStatorYoke"], - ["Length", "0mm"], - ["Skew", "0deg"], - ["Slots", "SlotNumber"], - ["SlotType", "SlotType"], - ["Hs0", "Hs0"], - ["Hs01", "0mm"], - ["Hs1", "Hs1"], - ["Hs2", "Hs2"], - ["Bs0", "Bs0"], - ["Bs1", "Bs1"], - ["Bs2", "Bs2"], - ["Rs", "Rs"], - ["FilletType", "0"], - ["HalfSlot", "0"], - ["SegAngle", "0deg"], - ["LenRegion", "0mm"], - ["InfoCore", "0"], - ] - - def set_output_vars(self): - self.output_vars = { - "pos": "(Moving1.Position -InitPos) * Poles/2", - "cos0": "cos(pos)", - "cos1": "cos(pos-2*PI/3)", - "cos2": "cos(pos-4*PI/3)", - "sin0": "sin(pos)", - "sin1": "sin(pos-2*PI/3)", - "sin2": "sin(pos-4*PI/3)", - "Lad": "L(PhaseA,PhaseA)*cos0 + L(PhaseA,PhaseB)*cos1 + L(PhaseA,PhaseC)*cos2", - "Laq": "L(PhaseA,PhaseA)*sin0 + L(PhaseA,PhaseB)*sin1 + L(PhaseA,PhaseC)*sin2", - "Lbd": "L(PhaseB,PhaseA)*cos0 + L(PhaseB,PhaseB)*cos1 + L(PhaseB,PhaseC)*cos2", - "Lbq": "L(PhaseB,PhaseA)*sin0 + L(PhaseB,PhaseB)*sin1 + L(PhaseB,PhaseC)*sin2", - "Lcd": "L(PhaseC,PhaseA)*cos0 + L(PhaseC,PhaseB)*cos1 + L(PhaseC,PhaseC)*cos2", - "Lcq": "L(PhaseC,PhaseA)*sin0 + L(PhaseC,PhaseB)*sin1 + L(PhaseC,PhaseC)*sin2", - "L_d": "(Lad*cos0 + Lbd*cos1 + Lcd*cos2) * 2/3", - "L_q": "(Laq*sin0 + Lbq*sin1 + Lcq*sin2) * 2/3", - "Flux_d": "(FluxLinkage(PhaseA)*cos0+FluxLinkage(PhaseB)*cos1+FluxLinkage(PhaseC)*cos2)*2/3", - "Flux_q": "-(FluxLinkage(PhaseA)*sin0+FluxLinkage(PhaseB)*sin1+FluxLinkage(PhaseC)*sin2)*2/3", - "Ui_d": "(InducedVoltage(PhaseA)*cos0+InducedVoltage(PhaseB)*cos1+InducedVoltage(PhaseC)*cos2)*2/3", - "Ui_q": "-(InducedVoltage(PhaseA)*sin0+InducedVoltage(PhaseB)*sin1+InducedVoltage(PhaseC)*sin2)*2/3", - "I_d": "(InputCurrent(PhaseA)*cos0 + InputCurrent(PhaseB)*cos1 + InputCurrent(PhaseC)*cos2)*2/3", - "I_q": "-(InputCurrent(PhaseA)*sin0 + InputCurrent(PhaseB)*sin1 + InputCurrent(PhaseC)*sin2)*2/3", - "Irms": "sqrt(I_d^2+I_q^2)/sqrt(2)", - } - - def set_post_params(self): - self.post_params = { # reports - ("InducedVoltage(PhaseA)", "InducedVoltage(PhaseB)", "InducedVoltage(PhaseC)"): "InducedVoltage", - ("Moving1.Torque"): "Torque", - ("InputCurrent(PhaseA)", "InputCurrent(PhaseB)", "InputCurrent(PhaseC)"): "Current", - ( - "FluxLinkage(PhaseA)", - "FluxLinkage(PhaseB)", - "FluxLinkage(PhaseC)", - ): "FluxLinkage", - ("I_d", "I_q"): "Current_dq", - ("Flux_d", "Flux_q"): "FluxLinkage_dq", - ("Ui_d", "Ui_q"): "InducedVoltage_dq", - ("L_d", "L_q"): "Inductance_dq", - } + # --- Attributes forwarded to geometry/computation for backward compatibility --- - def create_stator(self) -> None: - m2d = self.m2d - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - # Define design variables from the created dictionaries. - modeler.model_units = "mm" - for k, v in self.geom_params.items(): - m2d[k] = v - for k, v in self.wind_params.items(): - m2d[k] = v - for k, v in self.slot_params.items(): - m2d[k] = v - for k, v in self.mod_params.items(): - m2d[k] = v - for k, v in self.oper_params.items(): - m2d[k] = v - - # VACUUM OBJECTS - # Outer region - region_id = modeler.create_circle( - origin=[0, 0, 0], - radius="DiaStatorYoke/2", - # num_sides="SegAngle", - is_covered=True, - name="Region", - ) - # Band - band_id = modeler.create_circle( - origin=[0, 0, 0], - radius="(DiaStatorGap - (1.0 * Airgap))/2", - # num_sides="mapping_angle", - is_covered=True, - name="Band", - ) - # Shaft - shaft_id = modeler.create_circle( - origin=[0, 0, 0], - radius="DiaShaft/2", - is_covered=True, - name="Shaft", - ) - - # motion setup - m2d.assign_rotate_motion( - assignment="Band", - coordinate_system="Global", - axis="Z", - positive_movement=True, - start_position="InitPos", - angular_velocity="RotSpeed", - has_rotation_limits=False, - ) - - # Together - vacuum_obj_id = [ - shaft_id, - region_id, - band_id, - ] # put shaft first - for item in vacuum_obj_id: - item.color = (0, 255, 255) - item.transparency = 0.95 - - # Fit all view - modeler.fit_all() - - # Stator geometry - stator_id = modeler.create_udp( - dll="RMxprt/SlotCore.dll", - parameters=self.udp_par_list_stator, - library="syslib", - name="Stator", - # SolveInside="True", - ) - # Stator properties - stator_id.material_name = self.Fe - stator_id.color = (192, 192, 192) # rgb - stator_id.transparency = 0.0 - - # Winding - coil_id = modeler.create_rectangle( - origin=["DiaStatorGap/2+Hs0+Hs1+SlotLiner", "-(Bs1/2-SlotLiner)", 0], - sizes=["(Hs2+Rs/2-2*SlotLiner)/Layers-(Layers-1)*SpaceLayers/2", "Bs1-2*SlotLiner", 0], - name="Coil", - material="copper", - ) - coil_id.color = (255, 128, 0) - coil_id.transparency = 0.0 - modeler.rotate(assignment=coil_id, axis="Z", angle="360deg/SlotNumber/2") - coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", clones="SlotNumber/Poles", create_new_objects=True) - id_coils = modeler.get_objects_w_string(string_name="Coil", case_sensitive=True) - - # Create section of machine - object_list = [stator_id] + vacuum_obj_id - modeler.create_coordinate_system( - origin=[0, 0, 0], - reference_cs="Global", - name="Section", - mode="axis", - x_pointing=["cos(360deg/SymmetryFactor)", "sin(360deg/SymmetryFactor)", 0], - y_pointing=["-sin(360deg/SymmetryFactor)", "cos(360deg/SymmetryFactor)", 0], - ) - modeler.set_working_coordinate_system("Section") - modeler.split(assignment=object_list, plane="ZX", sides="NegativeOnly") - modeler.set_working_coordinate_system("Global") - modeler.split(assignment=object_list, plane="ZX", sides="PositiveOnly") - - # Symmetrical boundary conditions - pos_1 = "DiaStatorGap/4" - id_bc_1 = modeler.get_edgeid_from_position(position=[pos_1, 0, 0], assignment="Region") - id_bc_2 = modeler.get_edgeid_from_position( - position=[ - pos_1 + "*cos((360deg/SymmetryFactor))", - pos_1 + "*sin((360deg/SymmetryFactor))", - 0, - ], - assignment="Region", - ) - m2d.assign_master_slave( - independent=id_bc_1, - dependent=id_bc_2, - reverse_master=True, - reverse_slave=False, - same_as_master=False, - boundary="Symmetry", - ) - # Zero vector potential - pos_2 = "(DiaStatorYoke/2)" - id_bc_az = modeler.get_edgeid_from_position( - position=[ - pos_2 + "*cos((360deg/SymmetryFactor/2))", - pos_2 + "*sin((360deg/SymmetryFactor)/2)", - 0, - ], - assignment="Region", - ) - m2d.assign_vector_potential(assignment=id_bc_az, vector_value=0, boundary="A0") - - self.assign_stator_coils() - - # Mesh operation - m2d.mesh.assign_length_mesh( - assignment=id_coils, - inside_selection=True, - maximum_length=3, - maximum_elements=None, - name="coils", - ) - m2d.mesh.assign_length_mesh( - assignment=stator_id, - inside_selection=True, - maximum_length=3, - maximum_elements=None, - name="stator", - ) - - # core loss - m2d.set_core_losses("Stator", core_loss_on_field=False) - - # inductance calculation - self.inductance_computation() - - # model depth - m2d.model_depth = "StackLength" - # symmetry - m2d.change_symmetry_multiplier("SymmetryFactor") - # Calculation setup - setup = m2d.create_setup(name=self.setup_name) - setup.props["StopTime"] = "Nper/f" - setup.props["TimeStep"] = "1/(f*(PointPer-1))" - setup.props["SaveFieldsType"] = "None" - setup.props["OutputPerObjectCoreLoss"] = False - setup.props["OutputPerObjectSolidLoss"] = True - setup.props["OutputError"] = True - setup.update() - m2d.validate_simple() - - for k, v in self.output_vars.items(): - m2d.create_output_variable(k, v) - - for k, v in self.post_params.items(): - expressions = list(k) if isinstance(k, tuple) else [k] # if multiple report, use list(k). Else, use k - m2d.post.create_report( - expressions=expressions, - setup_sweep_name="", - domain="Sweep", - variations=None, - primary_sweep_variable="Time", - secondary_sweep_variable=None, - report_category=None, - plot_type="Rectangular Plot", - context=None, - subdesign_id=None, - polyline_points=1001, - plot_name=v, - ) - - def assign_stator_coils(self): - m2d = self.m2d - - # Excitations - I_A = "Im * cos(2*pi*f*time+epsI)" - I_B = "Im * cos(2*pi*f*time-120deg+epsI)" - I_C = "Im * cos(2*pi*f*time-240deg+epsI)" - - # Define phase windings - m2d.assign_coil( - assignment=["Coil"], - conductors_number="Nc", - polarity="Positive", - name="CS1", - ) - m2d.assign_coil( - assignment=["Coil_1"], - conductors_number="Nc", - polarity="Positive", - name="CS2", - ) - m2d.assign_coil( - assignment=["Coil_2"], - conductors_number="Nc", - polarity="Positive", - name="CS3", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_A, - parallel_branches="ParallelPaths", - name="PhaseA", - ) - m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS2", "CS3"]) - m2d.assign_coil( - assignment=["Coil_6"], - conductors_number="Nc", - polarity="Positive", - name="CS7", - ) - m2d.assign_coil( - assignment=["Coil_7"], - conductors_number="Nc", - polarity="Positive", - name="CS8", - ) - m2d.assign_coil( - assignment=["Coil_8"], - conductors_number="Nc", - polarity="Positive", - name="CS9", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_B, - parallel_branches="ParallelPaths", - name="PhaseB", - ) - m2d.add_winding_coils(assignment="PhaseB", coils=["CS7", "CS8", "CS9"]) - m2d.assign_coil( - assignment=["Coil_3"], - conductors_number="Nc", - polarity="Negative", - name="CS4", - ) - m2d.assign_coil( - assignment=["Coil_4"], - conductors_number="Nc", - polarity="Negative", - name="CS5", - ) - m2d.assign_coil( - assignment=["Coil_5"], - conductors_number="Nc", - polarity="Negative", - name="CS6", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_C, - parallel_branches="ParallelPaths", - name="PhaseC", - ) - m2d.add_winding_coils(assignment="PhaseC", coils=["CS4", "CS5", "CS6"]) - - def inductance_computation(self): - self.m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=False) + @property + def Fe(self): + return self.geometry.Fe - def add_rotor(self) -> None: - modeler = self.m2d.modeler - assert isinstance(modeler, Modeler2D) + @property + def geom_params(self): + return self.geometry.geom_params - rotor_id = modeler.create_polyline(points=self.rot_points, segment_type=["Arc", "Line", "Arc"], cover_surface=True, name="Rotor") - self.rotor_id = rotor_id - rotor_id.material_name = self.Fe - rotor_id.color = (192, 192, 192) # rgb - rotor_id.transparency = 0.0 + @property + def slot_params(self): + return self.geometry.slot_params - def add_rotor_barrier(self, barrier_points, segment_type=None) -> None: - modeler = self.m2d.modeler - assert isinstance(modeler, Modeler2D) + @property + def wind_params(self): + return self.geometry.wind_params - # Round it for Ansys - barrier_points = np.round(barrier_points, 6) + @property + def mod_params(self): + return self.geometry.mod_params - # Potentially add the z axis - if barrier_points.shape[1] == 2: - barrier_points = np.hstack((barrier_points, np.zeros((len(barrier_points), 1)))) + @property + def PolePairs(self): + return self.geometry.PolePairs - # Convert them into a string format and interpolate - points_str = [[str(y) for y in x] for x in barrier_points] - barrier_id = modeler.create_polyline(points=points_str, segment_type=segment_type, cover_surface=True, name="Barrier") + @property + def rot_points(self): + return self.geometry.rot_points - # Remove the barrier - self.rotor_id.subtract(barrier_id) - modeler.delete(barrier_id) + @property + def udp_par_list_stator(self): + return self.geometry.udp_par_list_stator - # TODO: change the other arguments to kwargs - def compute(self, *args, NUM_CORES: int = 1): - m2d = self.m2d - assert m2d.mesh is not None - assert m2d.post is not None + @property + def rotor_r_min(self): + return self.geometry.rotor_r_min - m2d.mesh.assign_length_mesh( - assignment=self.rotor_id, - inside_selection=True, - maximum_length=3, - maximum_elements=None, - name="rotor", - ) - # core loss rotor - m2d.set_core_losses("Rotor", core_loss_on_field=False) + @property + def rotor_r_max(self): + return self.geometry.rotor_r_max - self.set_variables(*args) + @property + def rotor_id(self): + return self.geometry.rotor_id - # Analyze - m2d.analyze_setup(self.setup_name, use_auto_settings=False, cores=NUM_CORES) + @property + def setup_name(self): + return self.computation.setup_name - solutions = m2d.post.get_solution_data(expressions=self.solution_expressions, primary_sweep_variable="Time") - try: - result = self.extract_results(solutions) - except AttributeError: - result = None + @property + def oper_params(self): + return self.computation.oper_params - # Delete solution data to prevent the saving size to explode - self.m2d.odesign.DeleteFullVariation("All", False) + @property + def solution_expressions(self): + return self.computation.solution_expressions - return result + @property + def output_vars(self): + return self.computation.output_vars - def set_variables(self, *args): - pass + @property + def post_params(self): + return self.computation.post_params - def extract_results(self, solutions): - return solutions.data_magnitude() + def mm_to_str(self, var, field) -> float: + return self.geometry.mm_to_str(var, field) + + # --- Orchestration --- + + def create_stator(self) -> None: + self.geometry.build_stator(self.m2d) + self.computation.assign_motion(self.m2d, band_name="Band") + self.computation.create_setup(self.m2d) + + def add_rotor(self) -> None: + self.geometry.build_rotor(self.m2d) + + def add_rotor_barrier(self, barrier_points, segment_type=None) -> None: + self.geometry.add_rotor_barrier(self.m2d, barrier_points, segment_type) def delete_rotor(self) -> None: - assert isinstance(self.m2d.modeler, Modeler2D) - self.m2d.modeler.delete(self.rotor_id) + self.geometry.delete_rotor(self.m2d) + + def compute(self, *args, NUM_CORES: int = 1): + return self.computation.compute(self.m2d, self.geometry.rotor_id, *args, NUM_CORES=NUM_CORES) def save_design(self, file_name: str, **kwargs) -> None: show = kwargs.pop("show", False) @@ -557,9 +129,3 @@ def save_project(self, file_name: str | None = None) -> None: def close_project(self) -> None: self.m2d.close_desktop() - - def mm_to_str(self, var, field) -> float: - val = getattr(self, var)[field] - if not val.endswith("mm"): - raise Exception("val must end with mm") - return float(val[:-2]) diff --git a/machine_design/design_computation.py b/machine_design/design_computation.py new file mode 100644 index 0000000..dcce157 --- /dev/null +++ b/machine_design/design_computation.py @@ -0,0 +1,281 @@ +from abc import ABC, abstractmethod + +from ansys.aedt.core import Maxwell2d + +from .design_geometry import GeometryBase + + +class ComputationBase(ABC): + def __init__(self, geometry: GeometryBase) -> None: + self.geometry = geometry + self.setup_name = "Setup1" + self.set_oper_params() + self.set_solution_expressions() + self.set_output_vars() + self.set_post_params() + + @abstractmethod + def set_oper_params(self): ... + + @abstractmethod + def set_solution_expressions(self): ... + + @abstractmethod + def set_output_vars(self): ... + + @abstractmethod + def set_post_params(self): ... + + @abstractmethod + def assign_stator_coils(self, m2d: Maxwell2d) -> None: ... + + @abstractmethod + def inductance_computation(self, m2d: Maxwell2d) -> None: ... + + @abstractmethod + def set_variables(self, m2d: Maxwell2d, *args): ... + + @abstractmethod + def extract_results(self, solutions): ... + + def assign_motion(self, m2d: Maxwell2d, band_name: str = "Band") -> None: + m2d.assign_rotate_motion( + assignment=band_name, + coordinate_system="Global", + axis="Z", + positive_movement=True, + start_position="InitPos", + angular_velocity="RotSpeed", + has_rotation_limits=False, + ) + + def create_setup(self, m2d: Maxwell2d) -> None: + for k, v in self.oper_params.items(): + m2d[k] = v + + self.assign_stator_coils(m2d) + self.inductance_computation(m2d) + + # model depth + m2d.model_depth = "StackLength" + # symmetry + m2d.change_symmetry_multiplier("SymmetryFactor") + # Calculation setup + setup = m2d.create_setup(name=self.setup_name) + setup.props["StopTime"] = "Nper/f" + setup.props["TimeStep"] = "1/(f*(PointPer-1))" + setup.props["SaveFieldsType"] = "None" + setup.props["OutputPerObjectCoreLoss"] = False + setup.props["OutputPerObjectSolidLoss"] = True + setup.props["OutputError"] = True + setup.update() + m2d.validate_simple() + + for k, v in self.output_vars.items(): + m2d.create_output_variable(k, v) + + for k, v in self.post_params.items(): + expressions = list(k) if isinstance(k, tuple) else [k] # if multiple report, use list(k). Else, use k + m2d.post.create_report( + expressions=expressions, + setup_sweep_name="", + domain="Sweep", + variations=None, + primary_sweep_variable="Time", + secondary_sweep_variable=None, + report_category=None, + plot_type="Rectangular Plot", + context=None, + subdesign_id=None, + polyline_points=1001, + plot_name=v, + ) + + # TODO: change the other arguments to kwargs + def compute(self, m2d: Maxwell2d, rotor_id, *args, NUM_CORES: int = 1): + assert m2d.mesh is not None + assert m2d.post is not None + + m2d.mesh.assign_length_mesh( + assignment=rotor_id, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="rotor", + ) + # core loss rotor + m2d.set_core_losses("Rotor", core_loss_on_field=False) + + self.set_variables(m2d, *args) + + # Analyze + m2d.analyze_setup(self.setup_name, use_auto_settings=False, cores=NUM_CORES) + + solutions = m2d.post.get_solution_data(expressions=self.solution_expressions, primary_sweep_variable="Time") + try: + result = self.extract_results(solutions) + except AttributeError: + result = None + + # Delete solution data to prevent the saving size to explode + m2d.odesign.DeleteFullVariation("All", False) + + return result + + +class Computation(ComputationBase): + def set_oper_params(self): + f = 50 # [Hz] + RotSpeed = 60 * f / self.geometry.PolePairs # [rpm] + self.oper_params = { + "Im": "1.5*sqrt(2)A", + "epsI": "pi/4", # current angle + "InitPos": "-30deg", + "f": f"{f}Hz", + "RotSpeed": f"{RotSpeed}rpm", + "Nper": "1/6", # number of included periods + "PointPer": "101", # number of time points per period + } + + def set_solution_expressions(self): + self.solution_expressions = "Moving1.Torque" + + def set_output_vars(self): + self.output_vars = { + "pos": "(Moving1.Position -InitPos) * Poles/2", + "cos0": "cos(pos)", + "cos1": "cos(pos-2*PI/3)", + "cos2": "cos(pos-4*PI/3)", + "sin0": "sin(pos)", + "sin1": "sin(pos-2*PI/3)", + "sin2": "sin(pos-4*PI/3)", + "Lad": "L(PhaseA,PhaseA)*cos0 + L(PhaseA,PhaseB)*cos1 + L(PhaseA,PhaseC)*cos2", + "Laq": "L(PhaseA,PhaseA)*sin0 + L(PhaseA,PhaseB)*sin1 + L(PhaseA,PhaseC)*sin2", + "Lbd": "L(PhaseB,PhaseA)*cos0 + L(PhaseB,PhaseB)*cos1 + L(PhaseB,PhaseC)*cos2", + "Lbq": "L(PhaseB,PhaseA)*sin0 + L(PhaseB,PhaseB)*sin1 + L(PhaseB,PhaseC)*sin2", + "Lcd": "L(PhaseC,PhaseA)*cos0 + L(PhaseC,PhaseB)*cos1 + L(PhaseC,PhaseC)*cos2", + "Lcq": "L(PhaseC,PhaseA)*sin0 + L(PhaseC,PhaseB)*sin1 + L(PhaseC,PhaseC)*sin2", + "L_d": "(Lad*cos0 + Lbd*cos1 + Lcd*cos2) * 2/3", + "L_q": "(Laq*sin0 + Lbq*sin1 + Lcq*sin2) * 2/3", + "Flux_d": "(FluxLinkage(PhaseA)*cos0+FluxLinkage(PhaseB)*cos1+FluxLinkage(PhaseC)*cos2)*2/3", + "Flux_q": "-(FluxLinkage(PhaseA)*sin0+FluxLinkage(PhaseB)*sin1+FluxLinkage(PhaseC)*sin2)*2/3", + "Ui_d": "(InducedVoltage(PhaseA)*cos0+InducedVoltage(PhaseB)*cos1+InducedVoltage(PhaseC)*cos2)*2/3", + "Ui_q": "-(InducedVoltage(PhaseA)*sin0+InducedVoltage(PhaseB)*sin1+InducedVoltage(PhaseC)*sin2)*2/3", + "I_d": "(InputCurrent(PhaseA)*cos0 + InputCurrent(PhaseB)*cos1 + InputCurrent(PhaseC)*cos2)*2/3", + "I_q": "-(InputCurrent(PhaseA)*sin0 + InputCurrent(PhaseB)*sin1 + InputCurrent(PhaseC)*sin2)*2/3", + "Irms": "sqrt(I_d^2+I_q^2)/sqrt(2)", + } + + def set_post_params(self): + self.post_params = { # reports + ("InducedVoltage(PhaseA)", "InducedVoltage(PhaseB)", "InducedVoltage(PhaseC)"): "InducedVoltage", + ("Moving1.Torque"): "Torque", + ("InputCurrent(PhaseA)", "InputCurrent(PhaseB)", "InputCurrent(PhaseC)"): "Current", + ( + "FluxLinkage(PhaseA)", + "FluxLinkage(PhaseB)", + "FluxLinkage(PhaseC)", + ): "FluxLinkage", + ("I_d", "I_q"): "Current_dq", + ("Flux_d", "Flux_q"): "FluxLinkage_dq", + ("Ui_d", "Ui_q"): "InducedVoltage_dq", + ("L_d", "L_q"): "Inductance_dq", + } + + def assign_stator_coils(self, m2d: Maxwell2d) -> None: + # Excitations + I_A = "Im * cos(2*pi*f*time+epsI)" + I_B = "Im * cos(2*pi*f*time-120deg+epsI)" + I_C = "Im * cos(2*pi*f*time-240deg+epsI)" + + # Define phase windings + m2d.assign_coil( + assignment=["Coil"], + conductors_number="Nc", + polarity="Positive", + name="CS1", + ) + m2d.assign_coil( + assignment=["Coil_1"], + conductors_number="Nc", + polarity="Positive", + name="CS2", + ) + m2d.assign_coil( + assignment=["Coil_2"], + conductors_number="Nc", + polarity="Positive", + name="CS3", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_A, + parallel_branches="ParallelPaths", + name="PhaseA", + ) + m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS2", "CS3"]) + m2d.assign_coil( + assignment=["Coil_6"], + conductors_number="Nc", + polarity="Positive", + name="CS7", + ) + m2d.assign_coil( + assignment=["Coil_7"], + conductors_number="Nc", + polarity="Positive", + name="CS8", + ) + m2d.assign_coil( + assignment=["Coil_8"], + conductors_number="Nc", + polarity="Positive", + name="CS9", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_B, + parallel_branches="ParallelPaths", + name="PhaseB", + ) + m2d.add_winding_coils(assignment="PhaseB", coils=["CS7", "CS8", "CS9"]) + m2d.assign_coil( + assignment=["Coil_3"], + conductors_number="Nc", + polarity="Negative", + name="CS4", + ) + m2d.assign_coil( + assignment=["Coil_4"], + conductors_number="Nc", + polarity="Negative", + name="CS5", + ) + m2d.assign_coil( + assignment=["Coil_5"], + conductors_number="Nc", + polarity="Negative", + name="CS6", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_C, + parallel_branches="ParallelPaths", + name="PhaseC", + ) + m2d.add_winding_coils(assignment="PhaseC", coils=["CS4", "CS5", "CS6"]) + + def inductance_computation(self, m2d: Maxwell2d) -> None: + m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=False) + + def set_variables(self, m2d: Maxwell2d, *args): + pass + + def extract_results(self, solutions): + return solutions.data_magnitude() diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py new file mode 100644 index 0000000..c80837a --- /dev/null +++ b/machine_design/design_geometry.py @@ -0,0 +1,318 @@ +from abc import ABC, abstractmethod + +import numpy as np +from ansys.aedt.core import Maxwell2d +from ansys.aedt.core.modeler.modeler_2d import Modeler2D + + +class GeometryBase(ABC): + def __init__(self) -> None: + self.set_iron() + self.set_geom_params() + self.set_slot_params() + self.set_winds_params() + self.set_mod_params() + self.set_rot_points() + self.set_derived_params() + self.set_udp_par_list_stator() + + @abstractmethod + def set_iron(self): ... + + @abstractmethod + def set_geom_params(self): ... + + @abstractmethod + def set_slot_params(self): ... + + @abstractmethod + def set_winds_params(self): ... + + @abstractmethod + def set_mod_params(self): ... + + @abstractmethod + def set_rot_points(self): ... + + @abstractmethod + def set_derived_params(self): ... + + @abstractmethod + def set_udp_par_list_stator(self): ... + + @abstractmethod + def build_stator(self, m2d: Maxwell2d) -> None: ... + + @abstractmethod + def build_rotor(self, m2d: Maxwell2d) -> None: ... + + def mm_to_str(self, var, field) -> float: + val = getattr(self, var)[field] + if not val.endswith("mm"): + raise Exception("val must end with mm") + return float(val[:-2]) + + def add_rotor_barrier(self, m2d: Maxwell2d, barrier_points, segment_type=None) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + # Round it for Ansys + barrier_points = np.round(barrier_points, 6) + + # Potentially add the z axis + if barrier_points.shape[1] == 2: + barrier_points = np.hstack((barrier_points, np.zeros((len(barrier_points), 1)))) + + # Convert them into a string format and interpolate + points_str = [[str(y) for y in x] for x in barrier_points] + barrier_id = modeler.create_polyline(points=points_str, segment_type=segment_type, cover_surface=True, name="Barrier") + + # Remove the barrier + self.rotor_id.subtract(barrier_id) + modeler.delete(barrier_id) + + def delete_rotor(self, m2d: Maxwell2d) -> None: + assert isinstance(m2d.modeler, Modeler2D) + m2d.modeler.delete(self.rotor_id) + + +class Geometry(GeometryBase): + def set_iron(self): + self.Fe = "Cogent Power - M350-50A, B-H at 50Hz" + + def set_geom_params(self): + self.geom_params = { + "DiaStatorGap": "79mm", + "DiaStatorYoke": "125mm", + "Airgap": "0.225mm", + "SlotNumber": "36", + "SlotType": "3", + "DiaShaft": "25mm", + "StackLength": "85mm", + } + + def set_slot_params(self): + self.slot_params = { + "Hs0": "0.95mm", + "Hs1": "0.31mm", + "Hs2": "8.24mm", + "Bs0": "2.2mm", + "Bs1": "3.37mm", + "Bs2": "4.8mm", + "Rs": "1.5mm", + "SetAngle": "10deg", + } + + def set_winds_params(self): + self.wind_params = { + "Layers": "1", + "ParallelPaths": "1", + "CoilPitch": "9", # coil pitch in slots + "SlotLiner": "0.3mm", + "SpaceLayers": "0.2mm", + "Nc": "68", # turns per coil + } + + def set_mod_params(self): + self.PolePairs = 2 + self.mod_params = { + "Poles": f"2*{self.PolePairs}", + "ModelLength": "85mm", + "SymmetryFactor": "Poles", + "StatorSkewAngle": "0deg", + } + + def set_rot_points(self): + self.rot_points = [ + ["DiaShaft/2*cos(360deg/SymmetryFactor)", "DiaShaft/2*sin(360deg/SymmetryFactor)", "0mm"], + ["DiaShaft/2*cos(360deg/(2*SymmetryFactor))", "DiaShaft/2*sin(360deg/(2*SymmetryFactor))", "0mm"], + ["DiaShaft/2", "0mm", "0mm"], + ["DiaStatorGap/2-Airgap", "0mm", "0mm"], + ["(DiaStatorGap/2-Airgap)*cos(360deg/(2*SymmetryFactor))", "(DiaStatorGap/2-Airgap)*sin(360deg/(2*SymmetryFactor))", "0mm"], + ["(DiaStatorGap/2-Airgap)*cos(360deg/SymmetryFactor)", "(DiaStatorGap/2-Airgap)*sin(360deg/SymmetryFactor)", "0mm"], + ] + + def set_derived_params(self): + self.rotor_r_min = self.mm_to_str("geom_params", "DiaShaft") / 2 + self.rotor_r_max = self.mm_to_str("geom_params", "DiaStatorGap") / 2 - self.mm_to_str("geom_params", "Airgap") + + def set_udp_par_list_stator(self): + self.udp_par_list_stator = [ + ["DiaGap", "DiaStatorGap"], + ["DiaYoke", "DiaStatorYoke"], + ["Length", "0mm"], + ["Skew", "0deg"], + ["Slots", "SlotNumber"], + ["SlotType", "SlotType"], + ["Hs0", "Hs0"], + ["Hs01", "0mm"], + ["Hs1", "Hs1"], + ["Hs2", "Hs2"], + ["Bs0", "Bs0"], + ["Bs1", "Bs1"], + ["Bs2", "Bs2"], + ["Rs", "Rs"], + ["FilletType", "0"], + ["HalfSlot", "0"], + ["SegAngle", "0deg"], + ["LenRegion", "0mm"], + ["InfoCore", "0"], + ] + + def build_stator(self, m2d: Maxwell2d) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + # Define design variables from the created dictionaries. + modeler.model_units = "mm" + for k, v in self.geom_params.items(): + m2d[k] = v + for k, v in self.wind_params.items(): + m2d[k] = v + for k, v in self.slot_params.items(): + m2d[k] = v + for k, v in self.mod_params.items(): + m2d[k] = v + + # VACUUM OBJECTS + # Outer region + region_id = modeler.create_circle( + origin=[0, 0, 0], + radius="DiaStatorYoke/2", + # num_sides="SegAngle", + is_covered=True, + name="Region", + ) + # Band + band_id = modeler.create_circle( + origin=[0, 0, 0], + radius="(DiaStatorGap - (1.0 * Airgap))/2", + # num_sides="mapping_angle", + is_covered=True, + name="Band", + ) + # Shaft + shaft_id = modeler.create_circle( + origin=[0, 0, 0], + radius="DiaShaft/2", + is_covered=True, + name="Shaft", + ) + + # Together + vacuum_obj_id = [ + shaft_id, + region_id, + band_id, + ] # put shaft first + for item in vacuum_obj_id: + item.color = (0, 255, 255) + item.transparency = 0.95 + + # Fit all view + modeler.fit_all() + + # Stator geometry + stator_id = modeler.create_udp( + dll="RMxprt/SlotCore.dll", + parameters=self.udp_par_list_stator, + library="syslib", + name="Stator", + # SolveInside="True", + ) + # Stator properties + stator_id.material_name = self.Fe + stator_id.color = (192, 192, 192) # rgb + stator_id.transparency = 0.0 + + # Winding + coil_id = modeler.create_rectangle( + origin=["DiaStatorGap/2+Hs0+Hs1+SlotLiner", "-(Bs1/2-SlotLiner)", 0], + sizes=["(Hs2+Rs/2-2*SlotLiner)/Layers-(Layers-1)*SpaceLayers/2", "Bs1-2*SlotLiner", 0], + name="Coil", + material="copper", + ) + coil_id.color = (255, 128, 0) + coil_id.transparency = 0.0 + modeler.rotate(assignment=coil_id, axis="Z", angle="360deg/SlotNumber/2") + coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", clones="SlotNumber/Poles", create_new_objects=True) + id_coils = modeler.get_objects_w_string(string_name="Coil", case_sensitive=True) + + # Create section of machine + object_list = [stator_id] + vacuum_obj_id + modeler.create_coordinate_system( + origin=[0, 0, 0], + reference_cs="Global", + name="Section", + mode="axis", + x_pointing=["cos(360deg/SymmetryFactor)", "sin(360deg/SymmetryFactor)", 0], + y_pointing=["-sin(360deg/SymmetryFactor)", "cos(360deg/SymmetryFactor)", 0], + ) + modeler.set_working_coordinate_system("Section") + modeler.split(assignment=object_list, plane="ZX", sides="NegativeOnly") + modeler.set_working_coordinate_system("Global") + modeler.split(assignment=object_list, plane="ZX", sides="PositiveOnly") + + # Symmetrical boundary conditions + pos_1 = "DiaStatorGap/4" + id_bc_1 = modeler.get_edgeid_from_position(position=[pos_1, 0, 0], assignment="Region") + id_bc_2 = modeler.get_edgeid_from_position( + position=[ + pos_1 + "*cos((360deg/SymmetryFactor))", + pos_1 + "*sin((360deg/SymmetryFactor))", + 0, + ], + assignment="Region", + ) + m2d.assign_master_slave( + independent=id_bc_1, + dependent=id_bc_2, + reverse_master=True, + reverse_slave=False, + same_as_master=False, + boundary="Symmetry", + ) + # Zero vector potential + pos_2 = "(DiaStatorYoke/2)" + id_bc_az = modeler.get_edgeid_from_position( + position=[ + pos_2 + "*cos((360deg/SymmetryFactor/2))", + pos_2 + "*sin((360deg/SymmetryFactor)/2)", + 0, + ], + assignment="Region", + ) + m2d.assign_vector_potential(assignment=id_bc_az, vector_value=0, boundary="A0") + + # Mesh operation + m2d.mesh.assign_length_mesh( + assignment=id_coils, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="coils", + ) + m2d.mesh.assign_length_mesh( + assignment=stator_id, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="stator", + ) + + # core loss + m2d.set_core_losses("Stator", core_loss_on_field=False) + + self.stator_id = stator_id + self.band_id = band_id + self.id_coils = id_coils + + def build_rotor(self, m2d: Maxwell2d) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + rotor_id = modeler.create_polyline(points=self.rot_points, segment_type=["Arc", "Line", "Arc"], cover_surface=True, name="Rotor") + self.rotor_id = rotor_id + rotor_id.material_name = self.Fe + rotor_id.color = (192, 192, 192) # rgb + rotor_id.transparency = 0.0 From af27a0d5461bc260b44b29704bd907e372e8dcfe Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 13:14:19 +0000 Subject: [PATCH 04/29] Refactor --- machine_design/design2.py | 325 --------------------------- machine_design/design_computation.py | 322 ++++++++++++++++++++++++++ machine_design/design_geometry.py | 19 ++ 3 files changed, 341 insertions(+), 325 deletions(-) delete mode 100644 machine_design/design2.py diff --git a/machine_design/design2.py b/machine_design/design2.py deleted file mode 100644 index 92fecf4..0000000 --- a/machine_design/design2.py +++ /dev/null @@ -1,325 +0,0 @@ -import numpy as np - -from .design import Design - - -class Design2(Design): - def set_geom_params(self): - super().set_geom_params() - self.geom_params["SlotNumber"] = "40" - - def set_slot_params(self): - super().set_slot_params() - self.slot_params["Bs1"] = "3.0mm" - self.slot_params["Bs2"] = "4.3mm" - self.slot_params["SetAngle"] = "9deg" - - def set_winds_params(self): - super().set_winds_params() - self.wind_params["Nc"] = "113" - - def set_oper_params(self): - f = 50 # [Hz] - RotSpeed = 60 * f / self.PolePairs # [rpm] - self.oper_params = { - "Id1": "0.0A", - "Iq1": "0.0A", - "Id3": "0.0A", - "Iq3": "0.0A", - "epsI1": "atan2(Iq1,Id1)", # current angle, 1st harmonic - "epsI3": "atan2(Iq3,Id3)", # current angle, 1st harmonic - "Im1": "sqrt(Id1^2+Iq1^2)", - "Im3": "sqrt(Id3^2+Iq3^2)", - "InitPos": "-45deg", - "f": f"{f}Hz", - "RotSpeed": f"{RotSpeed}rpm", - "Nper": "1/10", # number of included periods - "PointPer": "101", # number of time points per period - } - - def set_derived_params(self): - pass - - def set_solution_expressions(self): - self.solution_expressions = [ - "V_d1", - "V_q1", - "V_d3", - "V_q3", - "Flux_e_d1", - "Flux_e_q1", - "Flux_e_d3", - "Flux_e_q3", - "I_d1", - "I_q1", - "I_d3", - "I_q3", - "Ld1", - "Ld1q1", - "Ld1d3", - "Ld1q3", - "Lq1", - "Lq1d3", - "Lq1q3", - "Ld3", - "Ld3q3", - "Lq3", - "Moving1.Torque", - ] - - def set_output_vars(self): - self.output_vars = { - "PolePairs": "2", - "RotSign": "1", - "Rstat": "19", - "Lew": "0", - "theta_el": "RotSign*(Moving1.Position - InitPos) * PolePairs - pi", - "cos0_1": "cos(1*(theta_el - 2*PI*0/5))", - "sin0_1": "sin(-1*(theta_el - 2*PI*0/5))", - "cos1_1": "cos(1*(theta_el - 2*PI*1/5))", - "sin1_1": "sin(-1*(theta_el - 2*PI*1/5))", - "cos2_1": "cos(1*(theta_el - 2*PI*2/5))", - "sin2_1": "sin(-1*(theta_el - 2*PI*2/5))", - "cos3_1": "cos(1*(theta_el - 2*PI*3/5))", - "sin3_1": "sin(-1*(theta_el - 2*PI*3/5))", - "cos4_1": "cos(1*(theta_el - 2*PI*4/5))", - "sin4_1": "sin(-1*(theta_el - 2*PI*4/5))", - "cos0_3": "cos(3*(theta_el - 2*PI*0/5))", - "sin0_3": "sin(-3*(theta_el - 2*PI*0/5))", - "cos1_3": "cos(3*(theta_el - 2*PI*1/5))", - "sin1_3": "sin(-3*(theta_el - 2*PI*1/5))", - "cos2_3": "cos(3*(theta_el - 2*PI*2/5))", - "sin2_3": "sin(-3*(theta_el - 2*PI*2/5))", - "cos3_3": "cos(3*(theta_el - 2*PI*3/5))", - "sin3_3": "sin(-3*(theta_el - 2*PI*3/5))", - "cos4_3": "cos(3*(theta_el - 2*PI*4/5))", - "sin4_3": "sin(-3*(theta_el - 2*PI*4/5))", - "Flux_d1": "(FluxLinkage(PhaseA)*cos0_1 + FluxLinkage(PhaseB)*cos1_1 + FluxLinkage(PhaseC)*cos2_1 + FluxLinkage(PhaseD)*cos3_1 + FluxLinkage(PhaseE)*cos4_1) * 2/5", - "Flux_q1": "(FluxLinkage(PhaseA)*sin0_1 + FluxLinkage(PhaseB)*sin1_1 + FluxLinkage(PhaseC)*sin2_1 + FluxLinkage(PhaseD)*sin3_1 + FluxLinkage(PhaseE)*sin4_1) * 2/5", - "Flux_d3": "(FluxLinkage(PhaseA)*cos0_3 + FluxLinkage(PhaseB)*cos1_3 + FluxLinkage(PhaseC)*cos2_3 + FluxLinkage(PhaseD)*cos3_3 + FluxLinkage(PhaseE)*cos4_3) * 2/5", - "Flux_q3": "(FluxLinkage(PhaseA)*sin0_3 + FluxLinkage(PhaseB)*sin1_3 + FluxLinkage(PhaseC)*sin2_3 + FluxLinkage(PhaseD)*sin3_3 + FluxLinkage(PhaseE)*sin4_3) * 2/5", - "Vind_d1": "(InducedVoltage(PhaseA)*cos0_1 + InducedVoltage(PhaseB)*cos1_1 + InducedVoltage(PhaseC)*cos2_1 + InducedVoltage(PhaseD)*cos3_1 + InducedVoltage(PhaseE)*cos4_1) * 2/5", - "Vind_q1": "(InducedVoltage(PhaseA)*sin0_1 + InducedVoltage(PhaseB)*sin1_1 + InducedVoltage(PhaseC)*sin2_1 + InducedVoltage(PhaseD)*sin3_1 + InducedVoltage(PhaseE)*sin4_1) * 2/5", - "Vind_d3": "(InducedVoltage(PhaseA)*cos0_3 + InducedVoltage(PhaseB)*cos1_3 + InducedVoltage(PhaseC)*cos2_3 + InducedVoltage(PhaseD)*cos3_3 + InducedVoltage(PhaseE)*cos4_3) * 2/5", - "Vind_q3": "(InducedVoltage(PhaseA)*sin0_3 + InducedVoltage(PhaseB)*sin1_3 + InducedVoltage(PhaseC)*sin2_3 + InducedVoltage(PhaseD)*sin3_3 + InducedVoltage(PhaseE)*sin4_3) * 2/5", - "V_A": "InducedVoltage(PhaseA) + Rstat*InputCurrent(PhaseA) + Lew*deriv(InputCurrent(PhaseA))", - "V_B": "InducedVoltage(PhaseB) + Rstat*InputCurrent(PhaseB) + Lew*deriv(InputCurrent(PhaseB))", - "V_C": "InducedVoltage(PhaseC) + Rstat*InputCurrent(PhaseC) + Lew*deriv(InputCurrent(PhaseC))", - "V_D": "InducedVoltage(PhaseD) + Rstat*InputCurrent(PhaseD) + Lew*deriv(InputCurrent(PhaseD))", - "V_E": "InducedVoltage(PhaseE) + Rstat*InputCurrent(PhaseE) + Lew*deriv(InputCurrent(PhaseE))", - "V_AC": "V_A - V_C", - "V_BD": "V_B - V_D", - "V_CE": "V_C - V_E", - "V_DA": "V_D - V_A", - "V_EB": "V_E - V_B", - "Vterm_A": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", - "Vterm_B": "1/5*(2*V_AC + 4*V_BD + 1*V_CE + 3*V_DA)", - "Vterm_C": "1/5*(-3*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", - "Vterm_D": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + 3*V_DA)", - "Vterm_E": "1/5*(-3*V_AC + -1*V_BD + -4*V_CE + -2*V_DA)", - "I_d1": "(InputCurrent(PhaseA)*cos0_1 + InputCurrent(PhaseB)*cos1_1 + InputCurrent(PhaseC)*cos2_1 + InputCurrent(PhaseD)*cos3_1 + InputCurrent(PhaseE)*cos4_1) * 2/5", - "I_q1": "(InputCurrent(PhaseA)*sin0_1 + InputCurrent(PhaseB)*sin1_1 + InputCurrent(PhaseC)*sin2_1 + InputCurrent(PhaseD)*sin3_1 + InputCurrent(PhaseE)*sin4_1) * 2/5", - "I_d3": "(InputCurrent(PhaseA)*cos0_3 + InputCurrent(PhaseB)*cos1_3 + InputCurrent(PhaseC)*cos2_3 + InputCurrent(PhaseD)*cos3_3 + InputCurrent(PhaseE)*cos4_3) * 2/5", - "I_q3": "(InputCurrent(PhaseA)*sin0_3 + InputCurrent(PhaseB)*sin1_3 + InputCurrent(PhaseC)*sin2_3 + InputCurrent(PhaseD)*sin3_3 + InputCurrent(PhaseE)*sin4_3) * 2/5", - "V_d1": "(V_A*cos0_1 + V_B*cos1_1 + V_C*cos2_1 + V_D*cos3_1 + V_E*cos4_1) * 2/5", - "V_q1": "(V_A*sin0_1 + V_B*sin1_1 + V_C*sin2_1 + V_D*sin3_1 + V_E*sin4_1) * 2/5", - "V_d3": "(V_A*cos0_3 + V_B*cos1_3 + V_C*cos2_3 + V_D*cos3_3 + V_E*cos4_3) * 2/5", - "V_q3": "(V_A*sin0_3 + V_B*sin1_3 + V_C*sin2_3 + V_D*sin3_3 + V_E*sin4_3) * 2/5", - "L0d_1": "L(PhaseA,PhaseA)*cos0_1 + L(PhaseA,PhaseB)*cos1_1 + L(PhaseA,PhaseC)*cos2_1 + L(PhaseA,PhaseD)*cos3_1 + L(PhaseA,PhaseE)*cos4_1", - "L0q_1": "L(PhaseA,PhaseA)*sin0_1 + L(PhaseA,PhaseB)*sin1_1 + L(PhaseA,PhaseC)*sin2_1 + L(PhaseA,PhaseD)*sin3_1 + L(PhaseA,PhaseE)*sin4_1", - "L1d_1": "L(PhaseB,PhaseA)*cos0_1 + L(PhaseB,PhaseB)*cos1_1 + L(PhaseB,PhaseC)*cos2_1 + L(PhaseB,PhaseD)*cos3_1 + L(PhaseB,PhaseE)*cos4_1", - "L1q_1": "L(PhaseB,PhaseA)*sin0_1 + L(PhaseB,PhaseB)*sin1_1 + L(PhaseB,PhaseC)*sin2_1 + L(PhaseB,PhaseD)*sin3_1 + L(PhaseB,PhaseE)*sin4_1", - "L2d_1": "L(PhaseC,PhaseA)*cos0_1 + L(PhaseC,PhaseB)*cos1_1 + L(PhaseC,PhaseC)*cos2_1 + L(PhaseC,PhaseD)*cos3_1 + L(PhaseC,PhaseE)*cos4_1", - "L2q_1": "L(PhaseC,PhaseA)*sin0_1 + L(PhaseC,PhaseB)*sin1_1 + L(PhaseC,PhaseC)*sin2_1 + L(PhaseC,PhaseD)*sin3_1 + L(PhaseC,PhaseE)*sin4_1", - "L3d_1": "L(PhaseD,PhaseA)*cos0_1 + L(PhaseD,PhaseB)*cos1_1 + L(PhaseD,PhaseC)*cos2_1 + L(PhaseD,PhaseD)*cos3_1 + L(PhaseD,PhaseE)*cos4_1", - "L3q_1": "L(PhaseD,PhaseA)*sin0_1 + L(PhaseD,PhaseB)*sin1_1 + L(PhaseD,PhaseC)*sin2_1 + L(PhaseD,PhaseD)*sin3_1 + L(PhaseD,PhaseE)*sin4_1", - "L4d_1": "L(PhaseE,PhaseA)*cos0_1 + L(PhaseE,PhaseB)*cos1_1 + L(PhaseE,PhaseC)*cos2_1 + L(PhaseE,PhaseD)*cos3_1 + L(PhaseE,PhaseE)*cos4_1", - "L4q_1": "L(PhaseE,PhaseA)*sin0_1 + L(PhaseE,PhaseB)*sin1_1 + L(PhaseE,PhaseC)*sin2_1 + L(PhaseE,PhaseD)*sin3_1 + L(PhaseE,PhaseE)*sin4_1", - "L0d_3": "L(PhaseA,PhaseA)*cos0_3 + L(PhaseA,PhaseB)*cos1_3 + L(PhaseA,PhaseC)*cos2_3 + L(PhaseA,PhaseD)*cos3_3 + L(PhaseA,PhaseE)*cos4_3", - "L0q_3": "L(PhaseA,PhaseA)*sin0_3 + L(PhaseA,PhaseB)*sin1_3 + L(PhaseA,PhaseC)*sin2_3 + L(PhaseA,PhaseD)*sin3_3 + L(PhaseA,PhaseE)*sin4_3", - "L1d_3": "L(PhaseB,PhaseA)*cos0_3 + L(PhaseB,PhaseB)*cos1_3 + L(PhaseB,PhaseC)*cos2_3 + L(PhaseB,PhaseD)*cos3_3 + L(PhaseB,PhaseE)*cos4_3", - "L1q_3": "L(PhaseB,PhaseA)*sin0_3 + L(PhaseB,PhaseB)*sin1_3 + L(PhaseB,PhaseC)*sin2_3 + L(PhaseB,PhaseD)*sin3_3 + L(PhaseB,PhaseE)*sin4_3", - "L2d_3": "L(PhaseC,PhaseA)*cos0_3 + L(PhaseC,PhaseB)*cos1_3 + L(PhaseC,PhaseC)*cos2_3 + L(PhaseC,PhaseD)*cos3_3 + L(PhaseC,PhaseE)*cos4_3", - "L2q_3": "L(PhaseC,PhaseA)*sin0_3 + L(PhaseC,PhaseB)*sin1_3 + L(PhaseC,PhaseC)*sin2_3 + L(PhaseC,PhaseD)*sin3_3 + L(PhaseC,PhaseE)*sin4_3", - "L3d_3": "L(PhaseD,PhaseA)*cos0_3 + L(PhaseD,PhaseB)*cos1_3 + L(PhaseD,PhaseC)*cos2_3 + L(PhaseD,PhaseD)*cos3_3 + L(PhaseD,PhaseE)*cos4_3", - "L3q_3": "L(PhaseD,PhaseA)*sin0_3 + L(PhaseD,PhaseB)*sin1_3 + L(PhaseD,PhaseC)*sin2_3 + L(PhaseD,PhaseD)*sin3_3 + L(PhaseD,PhaseE)*sin4_3", - "L4d_3": "L(PhaseE,PhaseA)*cos0_3 + L(PhaseE,PhaseB)*cos1_3 + L(PhaseE,PhaseC)*cos2_3 + L(PhaseE,PhaseD)*cos3_3 + L(PhaseE,PhaseE)*cos4_3", - "L4q_3": "L(PhaseE,PhaseA)*sin0_3 + L(PhaseE,PhaseB)*sin1_3 + L(PhaseE,PhaseC)*sin2_3 + L(PhaseE,PhaseD)*sin3_3 + L(PhaseE,PhaseE)*sin4_3", - "Ld1": "(L0d_1*cos0_1 + L1d_1*cos1_1 + L2d_1*cos2_1 + L3d_1*cos3_1 + L4d_1*cos4_1) * 2/5", - "Ld1q1": "(L0d_1*sin0_1 + L1d_1*sin1_1 + L2d_1*sin2_1 + L3d_1*sin3_1 + L4d_1*sin4_1) * 2/5", - "Lq1d1": "(L0q_1*cos0_1 + L1q_1*cos1_1 + L2q_1*cos2_1 + L3q_1*cos3_1 + L4q_1*cos4_1) * 2/5", - "Lq1": "(L0q_1*sin0_1 + L1q_1*sin1_1 + L2q_1*sin2_1 + L3q_1*sin3_1 + L4q_1*sin4_1) * 2/5", - "Ld1d3": "(L0d_1*cos0_3 + L1d_1*cos1_3 + L2d_1*cos2_3 + L3d_1*cos3_3 + L4d_1*cos4_3) * 2/5", - "Ld1q3": "(L0d_1*sin0_3 + L1d_1*sin1_3 + L2d_1*sin2_3 + L3d_1*sin3_3 + L4d_1*sin4_3) * 2/5", - "Lq1d3": "(L0q_1*cos0_3 + L1q_1*cos1_3 + L2q_1*cos2_3 + L3q_1*cos3_3 + L4q_1*cos4_3) * 2/5", - "Lq1q3": "(L0q_1*sin0_3 + L1q_1*sin1_3 + L2q_1*sin2_3 + L3q_1*sin3_3 + L4q_1*sin4_3) * 2/5", - "Ld3d1": "(L0d_3*cos0_1 + L1d_3*cos1_1 + L2d_3*cos2_1 + L3d_3*cos3_1 + L4d_3*cos4_1) * 2/5", - "Ld3q1": "(L0d_3*sin0_1 + L1d_3*sin1_1 + L2d_3*sin2_1 + L3d_3*sin3_1 + L4d_3*sin4_1) * 2/5", - "Lq3d1": "(L0q_3*cos0_1 + L1q_3*cos1_1 + L2q_3*cos2_1 + L3q_3*cos3_1 + L4q_3*cos4_1) * 2/5", - "Lq3q1": "(L0q_3*sin0_1 + L1q_3*sin1_1 + L2q_3*sin2_1 + L3q_3*sin3_1 + L4q_3*sin4_1) * 2/5", - "Ld3": "(L0d_3*cos0_3 + L1d_3*cos1_3 + L2d_3*cos2_3 + L3d_3*cos3_3 + L4d_3*cos4_3) * 2/5", - "Ld3q3": "(L0d_3*sin0_3 + L1d_3*sin1_3 + L2d_3*sin2_3 + L3d_3*sin3_3 + L4d_3*sin4_3) * 2/5", - "Lq3d3": "(L0q_3*cos0_3 + L1q_3*cos1_3 + L2q_3*cos2_3 + L3q_3*cos3_3 + L4q_3*cos4_3) * 2/5", - "Lq3": "(L0q_3*sin0_3 + L1q_3*sin1_3 + L2q_3*sin2_3 + L3q_3*sin3_3 + L4q_3*sin4_3) * 2/5", - "Flux_e_d1": "Flux_d1 - (Ld1*I_d1 + Ld1q1*I_q1 + Ld1d3*I_d3 + Ld1q3*I_q3)", - "Flux_e_q1": "Flux_q1 - (Lq1d1*I_d1 + Lq1*I_q1 + Lq1d3*I_d3 + Lq1q3*I_q3)", - "Flux_e_d3": "Flux_d3 - (Ld3d1*I_d1 + Ld3q1*I_q1 + Ld3*I_d3 + Ld3q3*I_q3)", - "Flux_e_q3": "Flux_q3 - (Lq3d1*I_d1 + Lq3q1*I_q1 + Lq3d3*I_d3 + Lq3*I_q3)", - "Torque_dq": "5/2*PolePairs*(1*(Flux_d1*I_q1 - Flux_q1*I_d1) + 3*(Flux_d3*I_q3 - Flux_q3*I_d3))", - } - - def set_post_params(self): - self.post_params = { # reports - ("InducedVoltage(PhaseA)", "InducedVoltage(PhaseB)", "InducedVoltage(PhaseC)", "InducedVoltage(PhaseD)", "InducedVoltage(PhaseE)"): "InducedVoltage", - ("Moving1.Torque"): "Torque", - ("InputCurrent(PhaseA)", "InputCurrent(PhaseB)", "InputCurrent(PhaseC)", "InputCurrent(PhaseD)", "InputCurrent(PhaseE)"): "Current", - ("FluxLinkage(PhaseA)", "FluxLinkage(PhaseB)", "FluxLinkage(PhaseC)", "FluxLinkage(PhaseD)", "FluxLinkage(PhaseE)"): "FluxLinkage", - ("I_d1", "I_q1", "I_d3", "I_q3"): "Current_dq", - ("Flux_d1", "Flux_q1", "Flux_d3", "Flux_q3"): "FluxLinkage_dq", - ("Flux_e_d1", "Flux_e_q1", "Flux_e_d3", "Flux_e_q3"): "FluxLinkage excitation_dq", - ("Vind_d1", "Vind_q1", "Vind_d3", "Vind_q3"): "InducedVoltage_dq", - ("V_d1", "V_q1", "V_d3", "V_q3"): "TerminalVoltage_dq", - ("Ld1", "Lq1", "Ld3", "Lq3"): "Inductance_dq main", - ("Ld1q1", "Ld1d3", "Ld1q3", "Lq1d3", "Lq1q3", "Ld3q3"): "Inductance_dq cross-coupling", - } - - def assign_stator_coils(self): - m2d = self.m2d - - # Excitations - I_A = "Im1*cos(2*pi*f*time+epsI1-pi) + Im3*cos(3*(2*pi*f*time)+epsI3-pi)" - I_B = "Im1*cos(2*pi*f*time-72deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-72deg)+epsI3-pi)" - I_C = "Im1*cos(2*pi*f*time-144deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-144deg)+epsI3-pi)" - I_D = "Im1*cos(2*pi*f*time-216deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-216deg)+epsI3-pi)" - I_E = "Im1*cos(2*pi*f*time-288deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-288deg)+epsI3-pi)" - m2d.assign_coil - # Define phase windings - m2d.assign_coil( - assignment=["Coil"], - conductors_number="Nc", - polarity="Positive", - name="CS1", - ) - m2d.assign_coil( - assignment=["Coil_1"], - conductors_number="Nc", - polarity="Negative", - name="CS2", - ) - m2d.assign_coil( - assignment=["Coil_2"], - conductors_number="Nc", - polarity="Negative", - name="CS3", - ) - m2d.assign_coil( - assignment=["Coil_3"], - conductors_number="Nc", - polarity="Positive", - name="CS4", - ) - m2d.assign_coil( - assignment=["Coil_4"], - conductors_number="Nc", - polarity="Positive", - name="CS5", - ) - m2d.assign_coil( - assignment=["Coil_5"], - conductors_number="Nc", - polarity="Negative", - name="CS6", - ) - m2d.assign_coil( - assignment=["Coil_6"], - conductors_number="Nc", - polarity="Negative", - name="CS7", - ) - m2d.assign_coil( - assignment=["Coil_7"], - conductors_number="Nc", - polarity="Positive", - name="CS8", - ) - m2d.assign_coil( - assignment=["Coil_8"], - conductors_number="Nc", - polarity="Positive", - name="CS9", - ) - m2d.assign_coil( - assignment=["Coil_9"], - conductors_number="Nc", - polarity="Negative", - name="CS10", - ) - - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_A, - parallel_branches="ParallelPaths", - name="PhaseA", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_B, - parallel_branches="ParallelPaths", - name="PhaseB", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_C, - parallel_branches="ParallelPaths", - name="PhaseC", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_D, - parallel_branches="ParallelPaths", - name="PhaseD", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_E, - parallel_branches="ParallelPaths", - name="PhaseE", - ) - - m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS10"]) - m2d.add_winding_coils(assignment="PhaseB", coils=["CS4", "CS5"]) - m2d.add_winding_coils(assignment="PhaseC", coils=["CS8", "CS9"]) - m2d.add_winding_coils(assignment="PhaseD", coils=["CS2", "CS3"]) - m2d.add_winding_coils(assignment="PhaseE", coils=["CS6", "CS7"]) - - def inductance_computation(self): - self.m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=True) - - def set_variables(self, Id1, Iq1, Id3, Iq3): - self.m2d.variable_manager["Id1"] = f"{Id1}A" - self.m2d.variable_manager["Iq1"] = f"{Iq1}A" - self.m2d.variable_manager["Id3"] = f"{Id3}A" - self.m2d.variable_manager["Iq3"] = f"{Iq3}A" - - def extract_results(self, solutions): - # TODO: this works only because torque is the last one in the array - out = np.zeros(len(self.solution_expressions)) - for i, expr in enumerate(self.solution_expressions): - data = solutions.data_real(expr) - val = float(np.mean(data[:-1])) - - if expr.startswith("L_"): - val /= 1e9 - - out[i] = val - return data diff --git a/machine_design/design_computation.py b/machine_design/design_computation.py index dcce157..f068bf9 100644 --- a/machine_design/design_computation.py +++ b/machine_design/design_computation.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod +import numpy as np from ansys.aedt.core import Maxwell2d from .design_geometry import GeometryBase @@ -279,3 +280,324 @@ def set_variables(self, m2d: Maxwell2d, *args): def extract_results(self, solutions): return solutions.data_magnitude() + + +class Computation2(ComputationBase): + def set_oper_params(self): + f = 50 # [Hz] + RotSpeed = 60 * f / self.geometry.PolePairs # [rpm] + self.oper_params = { + "Id1": "0.0A", + "Iq1": "0.0A", + "Id3": "0.0A", + "Iq3": "0.0A", + "epsI1": "atan2(Iq1,Id1)", # current angle, 1st harmonic + "epsI3": "atan2(Iq3,Id3)", # current angle, 1st harmonic + "Im1": "sqrt(Id1^2+Iq1^2)", + "Im3": "sqrt(Id3^2+Iq3^2)", + "InitPos": "-45deg", + "f": f"{f}Hz", + "RotSpeed": f"{RotSpeed}rpm", + "Nper": "1/10", # number of included periods + "PointPer": "101", # number of time points per period + } + + def set_solution_expressions(self): + self.solution_expressions = [ + "V_d1", + "V_q1", + "V_d3", + "V_q3", + "Flux_e_d1", + "Flux_e_q1", + "Flux_e_d3", + "Flux_e_q3", + "I_d1", + "I_q1", + "I_d3", + "I_q3", + "Ld1", + "Ld1q1", + "Ld1d3", + "Ld1q3", + "Lq1", + "Lq1d3", + "Lq1q3", + "Ld3", + "Ld3q3", + "Lq3", + "Moving1.Torque", + ] + + def set_output_vars(self): + self.output_vars = { + "PolePairs": "2", + "RotSign": "1", + "Rstat": "19", + "Lew": "0", + "theta_el": "RotSign*(Moving1.Position - InitPos) * PolePairs - pi", + "cos0_1": "cos(1*(theta_el - 2*PI*0/5))", + "sin0_1": "sin(-1*(theta_el - 2*PI*0/5))", + "cos1_1": "cos(1*(theta_el - 2*PI*1/5))", + "sin1_1": "sin(-1*(theta_el - 2*PI*1/5))", + "cos2_1": "cos(1*(theta_el - 2*PI*2/5))", + "sin2_1": "sin(-1*(theta_el - 2*PI*2/5))", + "cos3_1": "cos(1*(theta_el - 2*PI*3/5))", + "sin3_1": "sin(-1*(theta_el - 2*PI*3/5))", + "cos4_1": "cos(1*(theta_el - 2*PI*4/5))", + "sin4_1": "sin(-1*(theta_el - 2*PI*4/5))", + "cos0_3": "cos(3*(theta_el - 2*PI*0/5))", + "sin0_3": "sin(-3*(theta_el - 2*PI*0/5))", + "cos1_3": "cos(3*(theta_el - 2*PI*1/5))", + "sin1_3": "sin(-3*(theta_el - 2*PI*1/5))", + "cos2_3": "cos(3*(theta_el - 2*PI*2/5))", + "sin2_3": "sin(-3*(theta_el - 2*PI*2/5))", + "cos3_3": "cos(3*(theta_el - 2*PI*3/5))", + "sin3_3": "sin(-3*(theta_el - 2*PI*3/5))", + "cos4_3": "cos(3*(theta_el - 2*PI*4/5))", + "sin4_3": "sin(-3*(theta_el - 2*PI*4/5))", + "Flux_d1": "(FluxLinkage(PhaseA)*cos0_1 + FluxLinkage(PhaseB)*cos1_1 + FluxLinkage(PhaseC)*cos2_1 + FluxLinkage(PhaseD)*cos3_1 + FluxLinkage(PhaseE)*cos4_1) * 2/5", + "Flux_q1": "(FluxLinkage(PhaseA)*sin0_1 + FluxLinkage(PhaseB)*sin1_1 + FluxLinkage(PhaseC)*sin2_1 + FluxLinkage(PhaseD)*sin3_1 + FluxLinkage(PhaseE)*sin4_1) * 2/5", + "Flux_d3": "(FluxLinkage(PhaseA)*cos0_3 + FluxLinkage(PhaseB)*cos1_3 + FluxLinkage(PhaseC)*cos2_3 + FluxLinkage(PhaseD)*cos3_3 + FluxLinkage(PhaseE)*cos4_3) * 2/5", + "Flux_q3": "(FluxLinkage(PhaseA)*sin0_3 + FluxLinkage(PhaseB)*sin1_3 + FluxLinkage(PhaseC)*sin2_3 + FluxLinkage(PhaseD)*sin3_3 + FluxLinkage(PhaseE)*sin4_3) * 2/5", + "Vind_d1": "(InducedVoltage(PhaseA)*cos0_1 + InducedVoltage(PhaseB)*cos1_1 + InducedVoltage(PhaseC)*cos2_1 + InducedVoltage(PhaseD)*cos3_1 + InducedVoltage(PhaseE)*cos4_1) * 2/5", + "Vind_q1": "(InducedVoltage(PhaseA)*sin0_1 + InducedVoltage(PhaseB)*sin1_1 + InducedVoltage(PhaseC)*sin2_1 + InducedVoltage(PhaseD)*sin3_1 + InducedVoltage(PhaseE)*sin4_1) * 2/5", + "Vind_d3": "(InducedVoltage(PhaseA)*cos0_3 + InducedVoltage(PhaseB)*cos1_3 + InducedVoltage(PhaseC)*cos2_3 + InducedVoltage(PhaseD)*cos3_3 + InducedVoltage(PhaseE)*cos4_3) * 2/5", + "Vind_q3": "(InducedVoltage(PhaseA)*sin0_3 + InducedVoltage(PhaseB)*sin1_3 + InducedVoltage(PhaseC)*sin2_3 + InducedVoltage(PhaseD)*sin3_3 + InducedVoltage(PhaseE)*sin4_3) * 2/5", + "V_A": "InducedVoltage(PhaseA) + Rstat*InputCurrent(PhaseA) + Lew*deriv(InputCurrent(PhaseA))", + "V_B": "InducedVoltage(PhaseB) + Rstat*InputCurrent(PhaseB) + Lew*deriv(InputCurrent(PhaseB))", + "V_C": "InducedVoltage(PhaseC) + Rstat*InputCurrent(PhaseC) + Lew*deriv(InputCurrent(PhaseC))", + "V_D": "InducedVoltage(PhaseD) + Rstat*InputCurrent(PhaseD) + Lew*deriv(InputCurrent(PhaseD))", + "V_E": "InducedVoltage(PhaseE) + Rstat*InputCurrent(PhaseE) + Lew*deriv(InputCurrent(PhaseE))", + "V_AC": "V_A - V_C", + "V_BD": "V_B - V_D", + "V_CE": "V_C - V_E", + "V_DA": "V_D - V_A", + "V_EB": "V_E - V_B", + "Vterm_A": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", + "Vterm_B": "1/5*(2*V_AC + 4*V_BD + 1*V_CE + 3*V_DA)", + "Vterm_C": "1/5*(-3*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", + "Vterm_D": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + 3*V_DA)", + "Vterm_E": "1/5*(-3*V_AC + -1*V_BD + -4*V_CE + -2*V_DA)", + "I_d1": "(InputCurrent(PhaseA)*cos0_1 + InputCurrent(PhaseB)*cos1_1 + InputCurrent(PhaseC)*cos2_1 + InputCurrent(PhaseD)*cos3_1 + InputCurrent(PhaseE)*cos4_1) * 2/5", + "I_q1": "(InputCurrent(PhaseA)*sin0_1 + InputCurrent(PhaseB)*sin1_1 + InputCurrent(PhaseC)*sin2_1 + InputCurrent(PhaseD)*sin3_1 + InputCurrent(PhaseE)*sin4_1) * 2/5", + "I_d3": "(InputCurrent(PhaseA)*cos0_3 + InputCurrent(PhaseB)*cos1_3 + InputCurrent(PhaseC)*cos2_3 + InputCurrent(PhaseD)*cos3_3 + InputCurrent(PhaseE)*cos4_3) * 2/5", + "I_q3": "(InputCurrent(PhaseA)*sin0_3 + InputCurrent(PhaseB)*sin1_3 + InputCurrent(PhaseC)*sin2_3 + InputCurrent(PhaseD)*sin3_3 + InputCurrent(PhaseE)*sin4_3) * 2/5", + "V_d1": "(V_A*cos0_1 + V_B*cos1_1 + V_C*cos2_1 + V_D*cos3_1 + V_E*cos4_1) * 2/5", + "V_q1": "(V_A*sin0_1 + V_B*sin1_1 + V_C*sin2_1 + V_D*sin3_1 + V_E*sin4_1) * 2/5", + "V_d3": "(V_A*cos0_3 + V_B*cos1_3 + V_C*cos2_3 + V_D*cos3_3 + V_E*cos4_3) * 2/5", + "V_q3": "(V_A*sin0_3 + V_B*sin1_3 + V_C*sin2_3 + V_D*sin3_3 + V_E*sin4_3) * 2/5", + "L0d_1": "L(PhaseA,PhaseA)*cos0_1 + L(PhaseA,PhaseB)*cos1_1 + L(PhaseA,PhaseC)*cos2_1 + L(PhaseA,PhaseD)*cos3_1 + L(PhaseA,PhaseE)*cos4_1", + "L0q_1": "L(PhaseA,PhaseA)*sin0_1 + L(PhaseA,PhaseB)*sin1_1 + L(PhaseA,PhaseC)*sin2_1 + L(PhaseA,PhaseD)*sin3_1 + L(PhaseA,PhaseE)*sin4_1", + "L1d_1": "L(PhaseB,PhaseA)*cos0_1 + L(PhaseB,PhaseB)*cos1_1 + L(PhaseB,PhaseC)*cos2_1 + L(PhaseB,PhaseD)*cos3_1 + L(PhaseB,PhaseE)*cos4_1", + "L1q_1": "L(PhaseB,PhaseA)*sin0_1 + L(PhaseB,PhaseB)*sin1_1 + L(PhaseB,PhaseC)*sin2_1 + L(PhaseB,PhaseD)*sin3_1 + L(PhaseB,PhaseE)*sin4_1", + "L2d_1": "L(PhaseC,PhaseA)*cos0_1 + L(PhaseC,PhaseB)*cos1_1 + L(PhaseC,PhaseC)*cos2_1 + L(PhaseC,PhaseD)*cos3_1 + L(PhaseC,PhaseE)*cos4_1", + "L2q_1": "L(PhaseC,PhaseA)*sin0_1 + L(PhaseC,PhaseB)*sin1_1 + L(PhaseC,PhaseC)*sin2_1 + L(PhaseC,PhaseD)*sin3_1 + L(PhaseC,PhaseE)*sin4_1", + "L3d_1": "L(PhaseD,PhaseA)*cos0_1 + L(PhaseD,PhaseB)*cos1_1 + L(PhaseD,PhaseC)*cos2_1 + L(PhaseD,PhaseD)*cos3_1 + L(PhaseD,PhaseE)*cos4_1", + "L3q_1": "L(PhaseD,PhaseA)*sin0_1 + L(PhaseD,PhaseB)*sin1_1 + L(PhaseD,PhaseC)*sin2_1 + L(PhaseD,PhaseD)*sin3_1 + L(PhaseD,PhaseE)*sin4_1", + "L4d_1": "L(PhaseE,PhaseA)*cos0_1 + L(PhaseE,PhaseB)*cos1_1 + L(PhaseE,PhaseC)*cos2_1 + L(PhaseE,PhaseD)*cos3_1 + L(PhaseE,PhaseE)*cos4_1", + "L4q_1": "L(PhaseE,PhaseA)*sin0_1 + L(PhaseE,PhaseB)*sin1_1 + L(PhaseE,PhaseC)*sin2_1 + L(PhaseE,PhaseD)*sin3_1 + L(PhaseE,PhaseE)*sin4_1", + "L0d_3": "L(PhaseA,PhaseA)*cos0_3 + L(PhaseA,PhaseB)*cos1_3 + L(PhaseA,PhaseC)*cos2_3 + L(PhaseA,PhaseD)*cos3_3 + L(PhaseA,PhaseE)*cos4_3", + "L0q_3": "L(PhaseA,PhaseA)*sin0_3 + L(PhaseA,PhaseB)*sin1_3 + L(PhaseA,PhaseC)*sin2_3 + L(PhaseA,PhaseD)*sin3_3 + L(PhaseA,PhaseE)*sin4_3", + "L1d_3": "L(PhaseB,PhaseA)*cos0_3 + L(PhaseB,PhaseB)*cos1_3 + L(PhaseB,PhaseC)*cos2_3 + L(PhaseB,PhaseD)*cos3_3 + L(PhaseB,PhaseE)*cos4_3", + "L1q_3": "L(PhaseB,PhaseA)*sin0_3 + L(PhaseB,PhaseB)*sin1_3 + L(PhaseB,PhaseC)*sin2_3 + L(PhaseB,PhaseD)*sin3_3 + L(PhaseB,PhaseE)*sin4_3", + "L2d_3": "L(PhaseC,PhaseA)*cos0_3 + L(PhaseC,PhaseB)*cos1_3 + L(PhaseC,PhaseC)*cos2_3 + L(PhaseC,PhaseD)*cos3_3 + L(PhaseC,PhaseE)*cos4_3", + "L2q_3": "L(PhaseC,PhaseA)*sin0_3 + L(PhaseC,PhaseB)*sin1_3 + L(PhaseC,PhaseC)*sin2_3 + L(PhaseC,PhaseD)*sin3_3 + L(PhaseC,PhaseE)*sin4_3", + "L3d_3": "L(PhaseD,PhaseA)*cos0_3 + L(PhaseD,PhaseB)*cos1_3 + L(PhaseD,PhaseC)*cos2_3 + L(PhaseD,PhaseD)*cos3_3 + L(PhaseD,PhaseE)*cos4_3", + "L3q_3": "L(PhaseD,PhaseA)*sin0_3 + L(PhaseD,PhaseB)*sin1_3 + L(PhaseD,PhaseC)*sin2_3 + L(PhaseD,PhaseD)*sin3_3 + L(PhaseD,PhaseE)*sin4_3", + "L4d_3": "L(PhaseE,PhaseA)*cos0_3 + L(PhaseE,PhaseB)*cos1_3 + L(PhaseE,PhaseC)*cos2_3 + L(PhaseE,PhaseD)*cos3_3 + L(PhaseE,PhaseE)*cos4_3", + "L4q_3": "L(PhaseE,PhaseA)*sin0_3 + L(PhaseE,PhaseB)*sin1_3 + L(PhaseE,PhaseC)*sin2_3 + L(PhaseE,PhaseD)*sin3_3 + L(PhaseE,PhaseE)*sin4_3", + "Ld1": "(L0d_1*cos0_1 + L1d_1*cos1_1 + L2d_1*cos2_1 + L3d_1*cos3_1 + L4d_1*cos4_1) * 2/5", + "Ld1q1": "(L0d_1*sin0_1 + L1d_1*sin1_1 + L2d_1*sin2_1 + L3d_1*sin3_1 + L4d_1*sin4_1) * 2/5", + "Lq1d1": "(L0q_1*cos0_1 + L1q_1*cos1_1 + L2q_1*cos2_1 + L3q_1*cos3_1 + L4q_1*cos4_1) * 2/5", + "Lq1": "(L0q_1*sin0_1 + L1q_1*sin1_1 + L2q_1*sin2_1 + L3q_1*sin3_1 + L4q_1*sin4_1) * 2/5", + "Ld1d3": "(L0d_1*cos0_3 + L1d_1*cos1_3 + L2d_1*cos2_3 + L3d_1*cos3_3 + L4d_1*cos4_3) * 2/5", + "Ld1q3": "(L0d_1*sin0_3 + L1d_1*sin1_3 + L2d_1*sin2_3 + L3d_1*sin3_3 + L4d_1*sin4_3) * 2/5", + "Lq1d3": "(L0q_1*cos0_3 + L1q_1*cos1_3 + L2q_1*cos2_3 + L3q_1*cos3_3 + L4q_1*cos4_3) * 2/5", + "Lq1q3": "(L0q_1*sin0_3 + L1q_1*sin1_3 + L2q_1*sin2_3 + L3q_1*sin3_3 + L4q_1*sin4_3) * 2/5", + "Ld3d1": "(L0d_3*cos0_1 + L1d_3*cos1_1 + L2d_3*cos2_1 + L3d_3*cos3_1 + L4d_3*cos4_1) * 2/5", + "Ld3q1": "(L0d_3*sin0_1 + L1d_3*sin1_1 + L2d_3*sin2_1 + L3d_3*sin3_1 + L4d_3*sin4_1) * 2/5", + "Lq3d1": "(L0q_3*cos0_1 + L1q_3*cos1_1 + L2q_3*cos2_1 + L3q_3*cos3_1 + L4q_3*cos4_1) * 2/5", + "Lq3q1": "(L0q_3*sin0_1 + L1q_3*sin1_1 + L2q_3*sin2_1 + L3q_3*sin3_1 + L4q_3*sin4_1) * 2/5", + "Ld3": "(L0d_3*cos0_3 + L1d_3*cos1_3 + L2d_3*cos2_3 + L3d_3*cos3_3 + L4d_3*cos4_3) * 2/5", + "Ld3q3": "(L0d_3*sin0_3 + L1d_3*sin1_3 + L2d_3*sin2_3 + L3d_3*sin3_3 + L4d_3*sin4_3) * 2/5", + "Lq3d3": "(L0q_3*cos0_3 + L1q_3*cos1_3 + L2q_3*cos2_3 + L3q_3*cos3_3 + L4q_3*cos4_3) * 2/5", + "Lq3": "(L0q_3*sin0_3 + L1q_3*sin1_3 + L2q_3*sin2_3 + L3q_3*sin3_3 + L4q_3*sin4_3) * 2/5", + "Flux_e_d1": "Flux_d1 - (Ld1*I_d1 + Ld1q1*I_q1 + Ld1d3*I_d3 + Ld1q3*I_q3)", + "Flux_e_q1": "Flux_q1 - (Lq1d1*I_d1 + Lq1*I_q1 + Lq1d3*I_d3 + Lq1q3*I_q3)", + "Flux_e_d3": "Flux_d3 - (Ld3d1*I_d1 + Ld3q1*I_q1 + Ld3*I_d3 + Ld3q3*I_q3)", + "Flux_e_q3": "Flux_q3 - (Lq3d1*I_d1 + Lq3q1*I_q1 + Lq3d3*I_d3 + Lq3*I_q3)", + "Torque_dq": "5/2*PolePairs*(1*(Flux_d1*I_q1 - Flux_q1*I_d1) + 3*(Flux_d3*I_q3 - Flux_q3*I_d3))", + } + + def set_post_params(self): + self.post_params = { # reports + ( + "InducedVoltage(PhaseA)", + "InducedVoltage(PhaseB)", + "InducedVoltage(PhaseC)", + "InducedVoltage(PhaseD)", + "InducedVoltage(PhaseE)", + ): "InducedVoltage", + ("Moving1.Torque"): "Torque", + ( + "InputCurrent(PhaseA)", + "InputCurrent(PhaseB)", + "InputCurrent(PhaseC)", + "InputCurrent(PhaseD)", + "InputCurrent(PhaseE)", + ): "Current", + ( + "FluxLinkage(PhaseA)", + "FluxLinkage(PhaseB)", + "FluxLinkage(PhaseC)", + "FluxLinkage(PhaseD)", + "FluxLinkage(PhaseE)", + ): "FluxLinkage", + ("I_d1", "I_q1", "I_d3", "I_q3"): "Current_dq", + ("Flux_d1", "Flux_q1", "Flux_d3", "Flux_q3"): "FluxLinkage_dq", + ("Flux_e_d1", "Flux_e_q1", "Flux_e_d3", "Flux_e_q3"): "FluxLinkage excitation_dq", + ("Vind_d1", "Vind_q1", "Vind_d3", "Vind_q3"): "InducedVoltage_dq", + ("V_d1", "V_q1", "V_d3", "V_q3"): "TerminalVoltage_dq", + ("Ld1", "Lq1", "Ld3", "Lq3"): "Inductance_dq main", + ("Ld1q1", "Ld1d3", "Ld1q3", "Lq1d3", "Lq1q3", "Ld3q3"): "Inductance_dq cross-coupling", + } + + def assign_stator_coils(self, m2d: Maxwell2d) -> None: + # Excitations + I_A = "Im1*cos(2*pi*f*time+epsI1-pi) + Im3*cos(3*(2*pi*f*time)+epsI3-pi)" + I_B = "Im1*cos(2*pi*f*time-72deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-72deg)+epsI3-pi)" + I_C = "Im1*cos(2*pi*f*time-144deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-144deg)+epsI3-pi)" + I_D = "Im1*cos(2*pi*f*time-216deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-216deg)+epsI3-pi)" + I_E = "Im1*cos(2*pi*f*time-288deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-288deg)+epsI3-pi)" + m2d.assign_coil + # Define phase windings + m2d.assign_coil( + assignment=["Coil"], + conductors_number="Nc", + polarity="Positive", + name="CS1", + ) + m2d.assign_coil( + assignment=["Coil_1"], + conductors_number="Nc", + polarity="Negative", + name="CS2", + ) + m2d.assign_coil( + assignment=["Coil_2"], + conductors_number="Nc", + polarity="Negative", + name="CS3", + ) + m2d.assign_coil( + assignment=["Coil_3"], + conductors_number="Nc", + polarity="Positive", + name="CS4", + ) + m2d.assign_coil( + assignment=["Coil_4"], + conductors_number="Nc", + polarity="Positive", + name="CS5", + ) + m2d.assign_coil( + assignment=["Coil_5"], + conductors_number="Nc", + polarity="Negative", + name="CS6", + ) + m2d.assign_coil( + assignment=["Coil_6"], + conductors_number="Nc", + polarity="Negative", + name="CS7", + ) + m2d.assign_coil( + assignment=["Coil_7"], + conductors_number="Nc", + polarity="Positive", + name="CS8", + ) + m2d.assign_coil( + assignment=["Coil_8"], + conductors_number="Nc", + polarity="Positive", + name="CS9", + ) + m2d.assign_coil( + assignment=["Coil_9"], + conductors_number="Nc", + polarity="Negative", + name="CS10", + ) + + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_A, + parallel_branches="ParallelPaths", + name="PhaseA", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_B, + parallel_branches="ParallelPaths", + name="PhaseB", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_C, + parallel_branches="ParallelPaths", + name="PhaseC", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_D, + parallel_branches="ParallelPaths", + name="PhaseD", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_E, + parallel_branches="ParallelPaths", + name="PhaseE", + ) + + m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS10"]) + m2d.add_winding_coils(assignment="PhaseB", coils=["CS4", "CS5"]) + m2d.add_winding_coils(assignment="PhaseC", coils=["CS8", "CS9"]) + m2d.add_winding_coils(assignment="PhaseD", coils=["CS2", "CS3"]) + m2d.add_winding_coils(assignment="PhaseE", coils=["CS6", "CS7"]) + + def inductance_computation(self, m2d: Maxwell2d) -> None: + m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=True) + + def set_variables(self, m2d: Maxwell2d, Id1, Iq1, Id3, Iq3): + m2d.variable_manager["Id1"] = f"{Id1}A" + m2d.variable_manager["Iq1"] = f"{Iq1}A" + m2d.variable_manager["Id3"] = f"{Id3}A" + m2d.variable_manager["Iq3"] = f"{Iq3}A" + + def extract_results(self, solutions): + # TODO: this works only because torque is the last one in the array + out = np.zeros(len(self.solution_expressions)) + for i, expr in enumerate(self.solution_expressions): + data = solutions.data_real(expr) + val = float(np.mean(data[:-1])) + + if expr.startswith("L_"): + val /= 1e9 + + out[i] = val + return data diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index c80837a..e85aaa8 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -316,3 +316,22 @@ def build_rotor(self, m2d: Maxwell2d) -> None: rotor_id.material_name = self.Fe rotor_id.color = (192, 192, 192) # rgb rotor_id.transparency = 0.0 + + +class Geometry2(Geometry): + def set_geom_params(self): + super().set_geom_params() + self.geom_params["SlotNumber"] = "40" + + def set_slot_params(self): + super().set_slot_params() + self.slot_params["Bs1"] = "3.0mm" + self.slot_params["Bs2"] = "4.3mm" + self.slot_params["SetAngle"] = "9deg" + + def set_winds_params(self): + super().set_winds_params() + self.wind_params["Nc"] = "113" + + def set_derived_params(self): + pass From edade449dedf425b7745c5099e30f63a61ff819c Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 13:16:28 +0000 Subject: [PATCH 05/29] Tests fixed --- tests/test_design2.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_design2.py b/tests/test_design2.py index 1438684..4e82d6c 100644 --- a/tests/test_design2.py +++ b/tests/test_design2.py @@ -1,7 +1,8 @@ import pytest from machine_design.design import Design -from machine_design.design2 import Design2 +from machine_design.design_computation import Computation2 +from machine_design.design_geometry import Geometry2 @pytest.fixture @@ -11,7 +12,9 @@ def base(): @pytest.fixture def design2(): - return Design2(m2d=None) + geometry = Geometry2() + computation = Computation2(geometry) + return Design(m2d=None, geometry=geometry, computation=computation) def test_geom_params_overrides_slot_number_only(base, design2): @@ -53,7 +56,7 @@ def test_oper_params_fully_replaced(design2): def test_derived_params_are_not_set(design2): - # Design2.set_derived_params() is a no-op, unlike Design's. + # Geometry2.set_derived_params() is a no-op, unlike Geometry's. assert not hasattr(design2, "rotor_r_min") assert not hasattr(design2, "rotor_r_max") From 11b8ade6fcc92eb7d47e8e3626945181fa553422 Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 13:24:51 +0000 Subject: [PATCH 06/29] FIxed tests --- machine_design/design.py | 18 +++++++++--------- tests/test_ansys_regression.py | 23 +++++++++++++++++++---- tests/test_design.py | 6 +++++- tests/test_design2.py | 8 +++++--- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/machine_design/design.py b/machine_design/design.py index 5c0c0e7..fe2c99b 100644 --- a/machine_design/design.py +++ b/machine_design/design.py @@ -1,32 +1,32 @@ from ansys.aedt.core import Desktop, Maxwell2d -from .design_computation import Computation, ComputationBase -from .design_geometry import Geometry, GeometryBase +from .design_computation import ComputationBase +from .design_geometry import GeometryBase class Design: - def __init__(self, m2d: Maxwell2d, geometry: GeometryBase | None = None, computation: ComputationBase | None = None) -> None: + def __init__(self, m2d: Maxwell2d, geometry: GeometryBase, computation: ComputationBase) -> None: self.m2d = m2d - self.geometry = geometry if geometry is not None else Geometry() - self.computation = computation if computation is not None else Computation(self.geometry) + self.geometry = geometry + self.computation = computation @classmethod - def create(cls, project_name: str, design_name: str, file_name: str, **kwargs) -> "Design": + def create(cls, project_name: str, design_name: str, file_name: str, geometry: GeometryBase, computation: ComputationBase, **kwargs) -> "Design": m2d = Maxwell2d(project=project_name, design=design_name, solution_type="TransientXY", **kwargs) - obj = cls(m2d) + obj = cls(m2d, geometry, computation) obj.create_stator() obj.save_project(file_name) return obj @classmethod - def load(cls, file_name: str, **kwargs) -> "Design": + def load(cls, file_name: str, geometry: GeometryBase, computation: ComputationBase, **kwargs) -> "Design": desktop = Desktop(**kwargs) desktop.load_project(file_name) m2d = Maxwell2d() m2d.set_active_design("Design01") - return cls(m2d) + return cls(m2d, geometry, computation) # --- Attributes forwarded to geometry/computation for backward compatibility --- diff --git a/tests/test_ansys_regression.py b/tests/test_ansys_regression.py index fb34003..9a9719f 100644 --- a/tests/test_ansys_regression.py +++ b/tests/test_ansys_regression.py @@ -16,6 +16,8 @@ import pytest from machine_design.design import Design as LiveDesign +from machine_design.design_computation import Computation +from machine_design.design_geometry import Geometry from machine_design.generators import HacklGenerator_OneLambda pytestmark = pytest.mark.ansys @@ -38,7 +40,8 @@ def _load_legacy_design_class(): def _generate_one_lambda_barriers(): # rotor_r_min/rotor_r_max are pure-Python derived params, so a throwaway # instance (no Ansys) is enough to size the generator. - dummy = LiveDesign(m2d=None) + geometry = Geometry() + dummy = LiveDesign(m2d=None, geometry=geometry, computation=Computation(geometry)) generator = HacklGenerator_OneLambda(dummy, R_STATOR_END, offset=OFFSET) np.random.seed(SEED) @@ -51,11 +54,12 @@ def _generate_one_lambda_barriers(): return barriers -def _build_and_compute(design_cls, project_name, file_name, barriers): +def _build_and_compute(design_cls, project_name, file_name, barriers, extra_args=()): design = design_cls.create( project_name, "Design01", file_name, + *extra_args, version=AEDT_VERSION, non_graphical=True, new_desktop=False, @@ -75,8 +79,19 @@ def test_legacy_and_live_design_produce_same_torque(tmp_path): legacy_design, live_design = None, None try: - legacy_design, torque_legacy = _build_and_compute(LegacyDesign, "RegressionTest_legacy", str(tmp_path / "legacy.aedt"), barriers) - live_design, torque_live = _build_and_compute(LiveDesign, "RegressionTest_live", str(tmp_path / "live.aedt"), barriers) + legacy_design, torque_legacy = _build_and_compute( + LegacyDesign, "RegressionTest_legacy", str(tmp_path / "legacy.aedt"), barriers + ) + + live_geometry = Geometry() + live_computation = Computation(live_geometry) + live_design, torque_live = _build_and_compute( + LiveDesign, + "RegressionTest_live", + str(tmp_path / "live.aedt"), + barriers, + extra_args=(live_geometry, live_computation), + ) assert torque_legacy is not None assert torque_live is not None diff --git a/tests/test_design.py b/tests/test_design.py index 1932ec7..bbc9d36 100644 --- a/tests/test_design.py +++ b/tests/test_design.py @@ -3,11 +3,15 @@ import pytest from machine_design.design import Design +from machine_design.design_computation import Computation +from machine_design.design_geometry import Geometry @pytest.fixture def design(): - return Design(m2d=None) + geometry = Geometry() + computation = Computation(geometry) + return Design(m2d=None, geometry=geometry, computation=computation) def test_setup_name(design): diff --git a/tests/test_design2.py b/tests/test_design2.py index 4e82d6c..99ac735 100644 --- a/tests/test_design2.py +++ b/tests/test_design2.py @@ -1,13 +1,15 @@ import pytest from machine_design.design import Design -from machine_design.design_computation import Computation2 -from machine_design.design_geometry import Geometry2 +from machine_design.design_computation import Computation, Computation2 +from machine_design.design_geometry import Geometry, Geometry2 @pytest.fixture def base(): - return Design(m2d=None) + geometry = Geometry() + computation = Computation(geometry) + return Design(m2d=None, geometry=geometry, computation=computation) @pytest.fixture From 341fc473c8bfbed2b27ac0de110b077193823a7c Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 13:44:39 +0000 Subject: [PATCH 07/29] Bugfix --- machine_design/design.py | 2 +- machine_design/design_computation.py | 14 ++------------ machine_design/design_geometry.py | 11 +++++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/machine_design/design.py b/machine_design/design.py index fe2c99b..00f9f6b 100644 --- a/machine_design/design.py +++ b/machine_design/design.py @@ -100,8 +100,8 @@ def mm_to_str(self, var, field) -> float: # --- Orchestration --- def create_stator(self) -> None: + self.computation.push_variables(self.m2d) self.geometry.build_stator(self.m2d) - self.computation.assign_motion(self.m2d, band_name="Band") self.computation.create_setup(self.m2d) def add_rotor(self) -> None: diff --git a/machine_design/design_computation.py b/machine_design/design_computation.py index f068bf9..c3302af 100644 --- a/machine_design/design_computation.py +++ b/machine_design/design_computation.py @@ -39,21 +39,11 @@ def set_variables(self, m2d: Maxwell2d, *args): ... @abstractmethod def extract_results(self, solutions): ... - def assign_motion(self, m2d: Maxwell2d, band_name: str = "Band") -> None: - m2d.assign_rotate_motion( - assignment=band_name, - coordinate_system="Global", - axis="Z", - positive_movement=True, - start_position="InitPos", - angular_velocity="RotSpeed", - has_rotation_limits=False, - ) - - def create_setup(self, m2d: Maxwell2d) -> None: + def push_variables(self, m2d: Maxwell2d) -> None: for k, v in self.oper_params.items(): m2d[k] = v + def create_setup(self, m2d: Maxwell2d) -> None: self.assign_stator_coils(m2d) self.inductance_computation(m2d) diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index e85aaa8..4821052 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -199,6 +199,17 @@ def build_stator(self, m2d: Maxwell2d) -> None: name="Shaft", ) + # motion setup + m2d.assign_rotate_motion( + assignment="Band", + coordinate_system="Global", + axis="Z", + positive_movement=True, + start_position="InitPos", + angular_velocity="RotSpeed", + has_rotation_limits=False, + ) + # Together vacuum_obj_id = [ shaft_id, From 33dfd4cbe037c95a6732297ca0557cf60863e507 Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 13:51:41 +0000 Subject: [PATCH 08/29] Refactor build_stator --- machine_design/design_geometry.py | 58 +++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index 4821052..efead22 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -160,10 +160,25 @@ def set_udp_par_list_stator(self): ] def build_stator(self, m2d: Maxwell2d) -> None: + self._push_stator_variables(m2d) + band_id, vacuum_obj_id = self._build_vacuum_regions(m2d) + stator_id = self._build_stator_core(m2d) + id_coils = self._build_stator_coils(m2d) + self._split_for_symmetry(m2d, [stator_id] + vacuum_obj_id) + self._assign_boundary_conditions(m2d) + self._assign_stator_mesh(m2d, stator_id, id_coils) + + # core loss + m2d.set_core_losses("Stator", core_loss_on_field=False) + + self.stator_id = stator_id + self.band_id = band_id + self.id_coils = id_coils + + def _push_stator_variables(self, m2d: Maxwell2d) -> None: modeler = m2d.modeler assert isinstance(modeler, Modeler2D) - # Define design variables from the created dictionaries. modeler.model_units = "mm" for k, v in self.geom_params.items(): m2d[k] = v @@ -174,7 +189,10 @@ def build_stator(self, m2d: Maxwell2d) -> None: for k, v in self.mod_params.items(): m2d[k] = v - # VACUUM OBJECTS + def _build_vacuum_regions(self, m2d: Maxwell2d): + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + # Outer region region_id = modeler.create_circle( origin=[0, 0, 0], @@ -223,7 +241,12 @@ def build_stator(self, m2d: Maxwell2d) -> None: # Fit all view modeler.fit_all() - # Stator geometry + return band_id, vacuum_obj_id + + def _build_stator_core(self, m2d: Maxwell2d): + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + stator_id = modeler.create_udp( dll="RMxprt/SlotCore.dll", parameters=self.udp_par_list_stator, @@ -231,12 +254,16 @@ def build_stator(self, m2d: Maxwell2d) -> None: name="Stator", # SolveInside="True", ) - # Stator properties stator_id.material_name = self.Fe stator_id.color = (192, 192, 192) # rgb stator_id.transparency = 0.0 - # Winding + return stator_id + + def _build_stator_coils(self, m2d: Maxwell2d): + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + coil_id = modeler.create_rectangle( origin=["DiaStatorGap/2+Hs0+Hs1+SlotLiner", "-(Bs1/2-SlotLiner)", 0], sizes=["(Hs2+Rs/2-2*SlotLiner)/Layers-(Layers-1)*SpaceLayers/2", "Bs1-2*SlotLiner", 0], @@ -249,8 +276,12 @@ def build_stator(self, m2d: Maxwell2d) -> None: coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", clones="SlotNumber/Poles", create_new_objects=True) id_coils = modeler.get_objects_w_string(string_name="Coil", case_sensitive=True) - # Create section of machine - object_list = [stator_id] + vacuum_obj_id + return id_coils + + def _split_for_symmetry(self, m2d: Maxwell2d, object_list) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + modeler.create_coordinate_system( origin=[0, 0, 0], reference_cs="Global", @@ -264,6 +295,10 @@ def build_stator(self, m2d: Maxwell2d) -> None: modeler.set_working_coordinate_system("Global") modeler.split(assignment=object_list, plane="ZX", sides="PositiveOnly") + def _assign_boundary_conditions(self, m2d: Maxwell2d) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + # Symmetrical boundary conditions pos_1 = "DiaStatorGap/4" id_bc_1 = modeler.get_edgeid_from_position(position=[pos_1, 0, 0], assignment="Region") @@ -295,7 +330,7 @@ def build_stator(self, m2d: Maxwell2d) -> None: ) m2d.assign_vector_potential(assignment=id_bc_az, vector_value=0, boundary="A0") - # Mesh operation + def _assign_stator_mesh(self, m2d: Maxwell2d, stator_id, id_coils) -> None: m2d.mesh.assign_length_mesh( assignment=id_coils, inside_selection=True, @@ -311,13 +346,6 @@ def build_stator(self, m2d: Maxwell2d) -> None: name="stator", ) - # core loss - m2d.set_core_losses("Stator", core_loss_on_field=False) - - self.stator_id = stator_id - self.band_id = band_id - self.id_coils = id_coils - def build_rotor(self, m2d: Maxwell2d) -> None: modeler = m2d.modeler assert isinstance(modeler, Modeler2D) From 2186598260750520dcd9577b2ec1ac3e7eaf885b Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 14:01:16 +0000 Subject: [PATCH 09/29] Refactor --- machine_design/design_geometry.py | 46 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index efead22..261a581 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -43,15 +43,22 @@ def set_udp_par_list_stator(self): ... @abstractmethod def build_stator(self, m2d: Maxwell2d) -> None: ... - @abstractmethod - def build_rotor(self, m2d: Maxwell2d) -> None: ... - def mm_to_str(self, var, field) -> float: val = getattr(self, var)[field] if not val.endswith("mm"): raise Exception("val must end with mm") return float(val[:-2]) + def build_rotor(self, m2d: Maxwell2d) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + rotor_id = modeler.create_polyline(points=self.rot_points, segment_type=["Arc", "Line", "Arc"], cover_surface=True, name="Rotor") + self.rotor_id = rotor_id + rotor_id.material_name = self.Fe + rotor_id.color = (192, 192, 192) # rgb + rotor_id.transparency = 0.0 + def add_rotor_barrier(self, m2d: Maxwell2d, barrier_points, segment_type=None) -> None: modeler = m2d.modeler assert isinstance(modeler, Modeler2D) @@ -162,6 +169,7 @@ def set_udp_par_list_stator(self): def build_stator(self, m2d: Maxwell2d) -> None: self._push_stator_variables(m2d) band_id, vacuum_obj_id = self._build_vacuum_regions(m2d) + self._assign_rotor_motion(m2d) stator_id = self._build_stator_core(m2d) id_coils = self._build_stator_coils(m2d) self._split_for_symmetry(m2d, [stator_id] + vacuum_obj_id) @@ -217,17 +225,6 @@ def _build_vacuum_regions(self, m2d: Maxwell2d): name="Shaft", ) - # motion setup - m2d.assign_rotate_motion( - assignment="Band", - coordinate_system="Global", - axis="Z", - positive_movement=True, - start_position="InitPos", - angular_velocity="RotSpeed", - has_rotation_limits=False, - ) - # Together vacuum_obj_id = [ shaft_id, @@ -243,6 +240,17 @@ def _build_vacuum_regions(self, m2d: Maxwell2d): return band_id, vacuum_obj_id + def _assign_rotor_motion(self, m2d: Maxwell2d) -> None: + m2d.assign_rotate_motion( + assignment="Band", + coordinate_system="Global", + axis="Z", + positive_movement=True, + start_position="InitPos", + angular_velocity="RotSpeed", + has_rotation_limits=False, + ) + def _build_stator_core(self, m2d: Maxwell2d): modeler = m2d.modeler assert isinstance(modeler, Modeler2D) @@ -346,16 +354,6 @@ def _assign_stator_mesh(self, m2d: Maxwell2d, stator_id, id_coils) -> None: name="stator", ) - def build_rotor(self, m2d: Maxwell2d) -> None: - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - rotor_id = modeler.create_polyline(points=self.rot_points, segment_type=["Arc", "Line", "Arc"], cover_surface=True, name="Rotor") - self.rotor_id = rotor_id - rotor_id.material_name = self.Fe - rotor_id.color = (192, 192, 192) # rgb - rotor_id.transparency = 0.0 - class Geometry2(Geometry): def set_geom_params(self): From 5bbdce36c2b3acff06e6ecae9cfc39584ce01fa1 Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 14:05:17 +0000 Subject: [PATCH 10/29] Refactor --- machine_design/design_geometry.py | 218 +++++++++++++++--------------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index 261a581..9e1e6e3 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -82,6 +82,115 @@ def delete_rotor(self, m2d: Maxwell2d) -> None: assert isinstance(m2d.modeler, Modeler2D) m2d.modeler.delete(self.rotor_id) + def _push_stator_variables(self, m2d: Maxwell2d) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + modeler.model_units = "mm" + for k, v in self.geom_params.items(): + m2d[k] = v + for k, v in self.wind_params.items(): + m2d[k] = v + for k, v in self.slot_params.items(): + m2d[k] = v + for k, v in self.mod_params.items(): + m2d[k] = v + + def _build_vacuum_regions(self, m2d: Maxwell2d): + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + # Outer region + region_id = modeler.create_circle( + origin=[0, 0, 0], + radius="DiaStatorYoke/2", + # num_sides="SegAngle", + is_covered=True, + name="Region", + ) + # Band + band_id = modeler.create_circle( + origin=[0, 0, 0], + radius="(DiaStatorGap - (1.0 * Airgap))/2", + # num_sides="mapping_angle", + is_covered=True, + name="Band", + ) + # Shaft + shaft_id = modeler.create_circle( + origin=[0, 0, 0], + radius="DiaShaft/2", + is_covered=True, + name="Shaft", + ) + + # Together + vacuum_obj_id = [ + shaft_id, + region_id, + band_id, + ] # put shaft first + for item in vacuum_obj_id: + item.color = (0, 255, 255) + item.transparency = 0.95 + + # Fit all view + modeler.fit_all() + + return band_id, vacuum_obj_id + + def _split_for_symmetry(self, m2d: Maxwell2d, object_list) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + modeler.create_coordinate_system( + origin=[0, 0, 0], + reference_cs="Global", + name="Section", + mode="axis", + x_pointing=["cos(360deg/SymmetryFactor)", "sin(360deg/SymmetryFactor)", 0], + y_pointing=["-sin(360deg/SymmetryFactor)", "cos(360deg/SymmetryFactor)", 0], + ) + modeler.set_working_coordinate_system("Section") + modeler.split(assignment=object_list, plane="ZX", sides="NegativeOnly") + modeler.set_working_coordinate_system("Global") + modeler.split(assignment=object_list, plane="ZX", sides="PositiveOnly") + + def _assign_boundary_conditions(self, m2d: Maxwell2d) -> None: + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + # Symmetrical boundary conditions + pos_1 = "DiaStatorGap/4" + id_bc_1 = modeler.get_edgeid_from_position(position=[pos_1, 0, 0], assignment="Region") + id_bc_2 = modeler.get_edgeid_from_position( + position=[ + pos_1 + "*cos((360deg/SymmetryFactor))", + pos_1 + "*sin((360deg/SymmetryFactor))", + 0, + ], + assignment="Region", + ) + m2d.assign_master_slave( + independent=id_bc_1, + dependent=id_bc_2, + reverse_master=True, + reverse_slave=False, + same_as_master=False, + boundary="Symmetry", + ) + # Zero vector potential + pos_2 = "(DiaStatorYoke/2)" + id_bc_az = modeler.get_edgeid_from_position( + position=[ + pos_2 + "*cos((360deg/SymmetryFactor/2))", + pos_2 + "*sin((360deg/SymmetryFactor)/2)", + 0, + ], + assignment="Region", + ) + m2d.assign_vector_potential(assignment=id_bc_az, vector_value=0, boundary="A0") + class Geometry(GeometryBase): def set_iron(self): @@ -183,63 +292,6 @@ def build_stator(self, m2d: Maxwell2d) -> None: self.band_id = band_id self.id_coils = id_coils - def _push_stator_variables(self, m2d: Maxwell2d) -> None: - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - modeler.model_units = "mm" - for k, v in self.geom_params.items(): - m2d[k] = v - for k, v in self.wind_params.items(): - m2d[k] = v - for k, v in self.slot_params.items(): - m2d[k] = v - for k, v in self.mod_params.items(): - m2d[k] = v - - def _build_vacuum_regions(self, m2d: Maxwell2d): - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - # Outer region - region_id = modeler.create_circle( - origin=[0, 0, 0], - radius="DiaStatorYoke/2", - # num_sides="SegAngle", - is_covered=True, - name="Region", - ) - # Band - band_id = modeler.create_circle( - origin=[0, 0, 0], - radius="(DiaStatorGap - (1.0 * Airgap))/2", - # num_sides="mapping_angle", - is_covered=True, - name="Band", - ) - # Shaft - shaft_id = modeler.create_circle( - origin=[0, 0, 0], - radius="DiaShaft/2", - is_covered=True, - name="Shaft", - ) - - # Together - vacuum_obj_id = [ - shaft_id, - region_id, - band_id, - ] # put shaft first - for item in vacuum_obj_id: - item.color = (0, 255, 255) - item.transparency = 0.95 - - # Fit all view - modeler.fit_all() - - return band_id, vacuum_obj_id - def _assign_rotor_motion(self, m2d: Maxwell2d) -> None: m2d.assign_rotate_motion( assignment="Band", @@ -286,58 +338,6 @@ def _build_stator_coils(self, m2d: Maxwell2d): return id_coils - def _split_for_symmetry(self, m2d: Maxwell2d, object_list) -> None: - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - modeler.create_coordinate_system( - origin=[0, 0, 0], - reference_cs="Global", - name="Section", - mode="axis", - x_pointing=["cos(360deg/SymmetryFactor)", "sin(360deg/SymmetryFactor)", 0], - y_pointing=["-sin(360deg/SymmetryFactor)", "cos(360deg/SymmetryFactor)", 0], - ) - modeler.set_working_coordinate_system("Section") - modeler.split(assignment=object_list, plane="ZX", sides="NegativeOnly") - modeler.set_working_coordinate_system("Global") - modeler.split(assignment=object_list, plane="ZX", sides="PositiveOnly") - - def _assign_boundary_conditions(self, m2d: Maxwell2d) -> None: - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - # Symmetrical boundary conditions - pos_1 = "DiaStatorGap/4" - id_bc_1 = modeler.get_edgeid_from_position(position=[pos_1, 0, 0], assignment="Region") - id_bc_2 = modeler.get_edgeid_from_position( - position=[ - pos_1 + "*cos((360deg/SymmetryFactor))", - pos_1 + "*sin((360deg/SymmetryFactor))", - 0, - ], - assignment="Region", - ) - m2d.assign_master_slave( - independent=id_bc_1, - dependent=id_bc_2, - reverse_master=True, - reverse_slave=False, - same_as_master=False, - boundary="Symmetry", - ) - # Zero vector potential - pos_2 = "(DiaStatorYoke/2)" - id_bc_az = modeler.get_edgeid_from_position( - position=[ - pos_2 + "*cos((360deg/SymmetryFactor/2))", - pos_2 + "*sin((360deg/SymmetryFactor)/2)", - 0, - ], - assignment="Region", - ) - m2d.assign_vector_potential(assignment=id_bc_az, vector_value=0, boundary="A0") - def _assign_stator_mesh(self, m2d: Maxwell2d, stator_id, id_coils) -> None: m2d.mesh.assign_length_mesh( assignment=id_coils, From 3f0ae1564f822d8698ea6ebc02edb3395d4da752 Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 15:45:40 +0000 Subject: [PATCH 11/29] Improved appearance --- machine_design/design_geometry.py | 37 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index 9e1e6e3..0487290 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -49,6 +49,20 @@ def mm_to_str(self, var, field) -> float: raise Exception("val must end with mm") return float(val[:-2]) + def _set_appearance(self, obj, color, transparency: float) -> None: + obj.color = color + obj.transparency = transparency + + def _set_appearances(self, m2d: Maxwell2d, vacuum_obj_id=None) -> None: + if vacuum_obj_id is not None: + for item in vacuum_obj_id: + self._set_appearance(item, (0, 255, 255), 0.95) + self._set_appearance(self.stator_id, (192, 192, 192), 0.0) + for name in self.id_coils: + self._set_appearance(m2d.modeler[name], (255, 128, 0), 0.0) + if hasattr(self, "rotor_id"): + self._set_appearance(self.rotor_id, (192, 192, 192), 0.0) + def build_rotor(self, m2d: Maxwell2d) -> None: modeler = m2d.modeler assert isinstance(modeler, Modeler2D) @@ -56,8 +70,7 @@ def build_rotor(self, m2d: Maxwell2d) -> None: rotor_id = modeler.create_polyline(points=self.rot_points, segment_type=["Arc", "Line", "Arc"], cover_surface=True, name="Rotor") self.rotor_id = rotor_id rotor_id.material_name = self.Fe - rotor_id.color = (192, 192, 192) # rgb - rotor_id.transparency = 0.0 + self._set_appearances(m2d) def add_rotor_barrier(self, m2d: Maxwell2d, barrier_points, segment_type=None) -> None: modeler = m2d.modeler @@ -124,20 +137,10 @@ def _build_vacuum_regions(self, m2d: Maxwell2d): name="Shaft", ) - # Together - vacuum_obj_id = [ - shaft_id, - region_id, - band_id, - ] # put shaft first - for item in vacuum_obj_id: - item.color = (0, 255, 255) - item.transparency = 0.95 - # Fit all view modeler.fit_all() - return band_id, vacuum_obj_id + return shaft_id, region_id, band_id def _split_for_symmetry(self, m2d: Maxwell2d, object_list) -> None: modeler = m2d.modeler @@ -277,7 +280,8 @@ def set_udp_par_list_stator(self): def build_stator(self, m2d: Maxwell2d) -> None: self._push_stator_variables(m2d) - band_id, vacuum_obj_id = self._build_vacuum_regions(m2d) + shaft_id, region_id, band_id = self._build_vacuum_regions(m2d) + vacuum_obj_id = [shaft_id, region_id, band_id] # put shaft first self._assign_rotor_motion(m2d) stator_id = self._build_stator_core(m2d) id_coils = self._build_stator_coils(m2d) @@ -291,6 +295,7 @@ def build_stator(self, m2d: Maxwell2d) -> None: self.stator_id = stator_id self.band_id = band_id self.id_coils = id_coils + self._set_appearances(m2d, vacuum_obj_id) def _assign_rotor_motion(self, m2d: Maxwell2d) -> None: m2d.assign_rotate_motion( @@ -315,8 +320,6 @@ def _build_stator_core(self, m2d: Maxwell2d): # SolveInside="True", ) stator_id.material_name = self.Fe - stator_id.color = (192, 192, 192) # rgb - stator_id.transparency = 0.0 return stator_id @@ -330,8 +333,6 @@ def _build_stator_coils(self, m2d: Maxwell2d): name="Coil", material="copper", ) - coil_id.color = (255, 128, 0) - coil_id.transparency = 0.0 modeler.rotate(assignment=coil_id, axis="Z", angle="360deg/SlotNumber/2") coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", clones="SlotNumber/Poles", create_new_objects=True) id_coils = modeler.get_objects_w_string(string_name="Coil", case_sensitive=True) From cc7143075a6be8bc63280e4b9e19dff8e86a71cf Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 17:26:04 +0000 Subject: [PATCH 12/29] Moved appearance --- machine_design/design_geometry.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index 0487290..2b511ed 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -53,15 +53,13 @@ def _set_appearance(self, obj, color, transparency: float) -> None: obj.color = color obj.transparency = transparency - def _set_appearances(self, m2d: Maxwell2d, vacuum_obj_id=None) -> None: - if vacuum_obj_id is not None: - for item in vacuum_obj_id: - self._set_appearance(item, (0, 255, 255), 0.95) + def set_appearances(self, m2d: Maxwell2d) -> None: + for item in [self.shaft_id, self.region_id, self.band_id]: + self._set_appearance(item, (0, 255, 255), 0.95) self._set_appearance(self.stator_id, (192, 192, 192), 0.0) for name in self.id_coils: self._set_appearance(m2d.modeler[name], (255, 128, 0), 0.0) - if hasattr(self, "rotor_id"): - self._set_appearance(self.rotor_id, (192, 192, 192), 0.0) + self._set_appearance(self.rotor_id, (192, 192, 192), 0.0) def build_rotor(self, m2d: Maxwell2d) -> None: modeler = m2d.modeler @@ -70,7 +68,6 @@ def build_rotor(self, m2d: Maxwell2d) -> None: rotor_id = modeler.create_polyline(points=self.rot_points, segment_type=["Arc", "Line", "Arc"], cover_surface=True, name="Rotor") self.rotor_id = rotor_id rotor_id.material_name = self.Fe - self._set_appearances(m2d) def add_rotor_barrier(self, m2d: Maxwell2d, barrier_points, segment_type=None) -> None: modeler = m2d.modeler @@ -281,11 +278,10 @@ def set_udp_par_list_stator(self): def build_stator(self, m2d: Maxwell2d) -> None: self._push_stator_variables(m2d) shaft_id, region_id, band_id = self._build_vacuum_regions(m2d) - vacuum_obj_id = [shaft_id, region_id, band_id] # put shaft first self._assign_rotor_motion(m2d) stator_id = self._build_stator_core(m2d) id_coils = self._build_stator_coils(m2d) - self._split_for_symmetry(m2d, [stator_id] + vacuum_obj_id) + self._split_for_symmetry(m2d, [stator_id, shaft_id, region_id, band_id]) self._assign_boundary_conditions(m2d) self._assign_stator_mesh(m2d, stator_id, id_coils) @@ -293,9 +289,10 @@ def build_stator(self, m2d: Maxwell2d) -> None: m2d.set_core_losses("Stator", core_loss_on_field=False) self.stator_id = stator_id + self.shaft_id = shaft_id + self.region_id = region_id self.band_id = band_id self.id_coils = id_coils - self._set_appearances(m2d, vacuum_obj_id) def _assign_rotor_motion(self, m2d: Maxwell2d) -> None: m2d.assign_rotate_motion( From 9f3dbba2d83a692d84abf46b7b373d03ffb7813f Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 17:26:12 +0000 Subject: [PATCH 13/29] Simplified assign_stator_coils --- machine_design/design_computation.py | 79 +++++++++------------------- 1 file changed, 24 insertions(+), 55 deletions(-) diff --git a/machine_design/design_computation.py b/machine_design/design_computation.py index c3302af..00db8a5 100644 --- a/machine_design/design_computation.py +++ b/machine_design/design_computation.py @@ -179,25 +179,13 @@ def assign_stator_coils(self, m2d: Maxwell2d) -> None: I_B = "Im * cos(2*pi*f*time-120deg+epsI)" I_C = "Im * cos(2*pi*f*time-240deg+epsI)" - # Define phase windings - m2d.assign_coil( - assignment=["Coil"], - conductors_number="Nc", - polarity="Positive", - name="CS1", - ) - m2d.assign_coil( - assignment=["Coil_1"], - conductors_number="Nc", - polarity="Positive", - name="CS2", - ) - m2d.assign_coil( - assignment=["Coil_2"], - conductors_number="Nc", - polarity="Positive", - name="CS3", - ) + for i in range(1, 4): + m2d.assign_coil( + assignment=[self.geometry.id_coils[i-1]], + conductors_number="Nc", + polarity="Positive", + name=f"CS{i}", + ) m2d.assign_winding( assignment=None, winding_type="Current", @@ -207,24 +195,14 @@ def assign_stator_coils(self, m2d: Maxwell2d) -> None: name="PhaseA", ) m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS2", "CS3"]) - m2d.assign_coil( - assignment=["Coil_6"], - conductors_number="Nc", - polarity="Positive", - name="CS7", - ) - m2d.assign_coil( - assignment=["Coil_7"], - conductors_number="Nc", - polarity="Positive", - name="CS8", - ) - m2d.assign_coil( - assignment=["Coil_8"], - conductors_number="Nc", - polarity="Positive", - name="CS9", - ) + + for i in range(7, 10): + m2d.assign_coil( + assignment=[self.geometry.id_coils[i-1]], + conductors_number="Nc", + polarity="Positive", + name=f"CS{i}", + ) m2d.assign_winding( assignment=None, winding_type="Current", @@ -234,24 +212,15 @@ def assign_stator_coils(self, m2d: Maxwell2d) -> None: name="PhaseB", ) m2d.add_winding_coils(assignment="PhaseB", coils=["CS7", "CS8", "CS9"]) - m2d.assign_coil( - assignment=["Coil_3"], - conductors_number="Nc", - polarity="Negative", - name="CS4", - ) - m2d.assign_coil( - assignment=["Coil_4"], - conductors_number="Nc", - polarity="Negative", - name="CS5", - ) - m2d.assign_coil( - assignment=["Coil_5"], - conductors_number="Nc", - polarity="Negative", - name="CS6", - ) + + + for i in range(4, 7): + m2d.assign_coil( + assignment=[self.geometry.id_coils[i-1]], + conductors_number="Nc", + polarity="Negative", + name=f"CS{i}", + ) m2d.assign_winding( assignment=None, winding_type="Current", From bb99e6685db7a99c4c931ff9699829f46d989ad9 Mon Sep 17 00:00:00 2001 From: sadda Date: Mon, 13 Jul 2026 18:01:01 +0000 Subject: [PATCH 14/29] Simplified assign_stator_coils --- machine_design/design_computation.py | 75 +++++++++------------------- 1 file changed, 24 insertions(+), 51 deletions(-) diff --git a/machine_design/design_computation.py b/machine_design/design_computation.py index 00db8a5..2b6bd5f 100644 --- a/machine_design/design_computation.py +++ b/machine_design/design_computation.py @@ -179,57 +179,30 @@ def assign_stator_coils(self, m2d: Maxwell2d) -> None: I_B = "Im * cos(2*pi*f*time-120deg+epsI)" I_C = "Im * cos(2*pi*f*time-240deg+epsI)" - for i in range(1, 4): - m2d.assign_coil( - assignment=[self.geometry.id_coils[i-1]], - conductors_number="Nc", - polarity="Positive", - name=f"CS{i}", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_A, - parallel_branches="ParallelPaths", - name="PhaseA", - ) - m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS2", "CS3"]) - - for i in range(7, 10): - m2d.assign_coil( - assignment=[self.geometry.id_coils[i-1]], - conductors_number="Nc", - polarity="Positive", - name=f"CS{i}", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_B, - parallel_branches="ParallelPaths", - name="PhaseB", - ) - m2d.add_winding_coils(assignment="PhaseB", coils=["CS7", "CS8", "CS9"]) - - - for i in range(4, 7): - m2d.assign_coil( - assignment=[self.geometry.id_coils[i-1]], - conductors_number="Nc", - polarity="Negative", - name=f"CS{i}", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_C, - parallel_branches="ParallelPaths", - name="PhaseC", - ) - m2d.add_winding_coils(assignment="PhaseC", coils=["CS4", "CS5", "CS6"]) + phases_polarity = ["Positive", "Negative", "Positive"] + phases_name = ["A", "C", "B"] + phases_current = [I_A, I_C, I_B] + i_coil = 0 + for phase_polarity, phase_name, phase_current in zip(phases_polarity, phases_name, phases_current): + names = [] + for _ in range(3): + m2d.assign_coil( + assignment=[self.geometry.id_coils[i_coil]], + conductors_number="Nc", + polarity=phase_polarity, + name=f"CS{i_coil+1}", + ) + names.append(f"CS{i_coil+1}") + i_coil += 1 + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=phase_current, + parallel_branches="ParallelPaths", + name=f"Phase{phase_name}", + ) + m2d.add_winding_coils(assignment=f"Phase{phase_name}", coils=names) def inductance_computation(self, m2d: Maxwell2d) -> None: m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=False) From 00c3448b9e8eb9d7c0a072811a7a1c4b2a616c72 Mon Sep 17 00:00:00 2001 From: sadda Date: Tue, 14 Jul 2026 07:35:54 +0000 Subject: [PATCH 15/29] Added safety check --- machine_design/design.py | 1 + 1 file changed, 1 insertion(+) diff --git a/machine_design/design.py b/machine_design/design.py index 00f9f6b..e6f0e08 100644 --- a/machine_design/design.py +++ b/machine_design/design.py @@ -6,6 +6,7 @@ class Design: def __init__(self, m2d: Maxwell2d, geometry: GeometryBase, computation: ComputationBase) -> None: + assert computation.geometry is geometry self.m2d = m2d self.geometry = geometry self.computation = computation From 6b28a0618eb29abfefd1540620a73ebd7e60ba68 Mon Sep 17 00:00:00 2001 From: sadda Date: Tue, 14 Jul 2026 07:39:24 +0000 Subject: [PATCH 16/29] Changed names --- machine_design/design.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/machine_design/design.py b/machine_design/design.py index e6f0e08..09fde98 100644 --- a/machine_design/design.py +++ b/machine_design/design.py @@ -15,7 +15,7 @@ def __init__(self, m2d: Maxwell2d, geometry: GeometryBase, computation: Computat def create(cls, project_name: str, design_name: str, file_name: str, geometry: GeometryBase, computation: ComputationBase, **kwargs) -> "Design": m2d = Maxwell2d(project=project_name, design=design_name, solution_type="TransientXY", **kwargs) obj = cls(m2d, geometry, computation) - obj.create_stator() + obj.add_stator() obj.save_project(file_name) return obj @@ -100,7 +100,7 @@ def mm_to_str(self, var, field) -> float: # --- Orchestration --- - def create_stator(self) -> None: + def add_stator(self) -> None: self.computation.push_variables(self.m2d) self.geometry.build_stator(self.m2d) self.computation.create_setup(self.m2d) From 32b5963ca510eb06c35329dd2233ee96914fba46 Mon Sep 17 00:00:00 2001 From: sadda Date: Tue, 14 Jul 2026 08:31:14 +0000 Subject: [PATCH 17/29] Removed unused properties --- machine_design/design.py | 71 ------------------------------ machine_design/generators.py | 4 +- machine_design/geometry.py | 2 +- tests/test_design.py | 84 ++++++++++++++++++------------------ tests/test_design2.py | 66 ++++++++++++++-------------- 5 files changed, 79 insertions(+), 148 deletions(-) diff --git a/machine_design/design.py b/machine_design/design.py index 09fde98..83f53e7 100644 --- a/machine_design/design.py +++ b/machine_design/design.py @@ -29,77 +29,6 @@ def load(cls, file_name: str, geometry: GeometryBase, computation: ComputationBa return cls(m2d, geometry, computation) - # --- Attributes forwarded to geometry/computation for backward compatibility --- - - @property - def Fe(self): - return self.geometry.Fe - - @property - def geom_params(self): - return self.geometry.geom_params - - @property - def slot_params(self): - return self.geometry.slot_params - - @property - def wind_params(self): - return self.geometry.wind_params - - @property - def mod_params(self): - return self.geometry.mod_params - - @property - def PolePairs(self): - return self.geometry.PolePairs - - @property - def rot_points(self): - return self.geometry.rot_points - - @property - def udp_par_list_stator(self): - return self.geometry.udp_par_list_stator - - @property - def rotor_r_min(self): - return self.geometry.rotor_r_min - - @property - def rotor_r_max(self): - return self.geometry.rotor_r_max - - @property - def rotor_id(self): - return self.geometry.rotor_id - - @property - def setup_name(self): - return self.computation.setup_name - - @property - def oper_params(self): - return self.computation.oper_params - - @property - def solution_expressions(self): - return self.computation.solution_expressions - - @property - def output_vars(self): - return self.computation.output_vars - - @property - def post_params(self): - return self.computation.post_params - - def mm_to_str(self, var, field) -> float: - return self.geometry.mm_to_str(var, field) - - # --- Orchestration --- - def add_stator(self) -> None: self.computation.push_variables(self.m2d) self.geometry.build_stator(self.m2d) diff --git a/machine_design/generators.py b/machine_design/generators.py index 0798ccb..7ffa39f 100644 --- a/machine_design/generators.py +++ b/machine_design/generators.py @@ -36,8 +36,8 @@ def save_params(params, file_name: str) -> None: class BarrierGenerator(ABC): # TODO: rename offset def __init__(self, design: Design, r_stator_end: float, offset: None | float = None, n_curve: int = 500, n_flat: int = 20) -> None: - self.r_max = design.rotor_r_max - self.r_min = design.rotor_r_min + self.r_max = design.geometry.rotor_r_max + self.r_min = design.geometry.rotor_r_min self.R = self.r_max - r_stator_end self.offset = offset self.n_curve = n_curve diff --git a/machine_design/geometry.py b/machine_design/geometry.py index ecac422..e628a16 100644 --- a/machine_design/geometry.py +++ b/machine_design/geometry.py @@ -17,7 +17,7 @@ def rotate(x, y, alpha, **kwargs): def plot_barriers(barriers, design, title=None, file_name=None): plt.figure() alphas = np.linspace(0 * np.pi, 2 * np.pi / 4, 100) - for r in [design.rotor_r_min, design.rotor_r_max]: + for r in [design.geometry.rotor_r_min, design.geometry.rotor_r_max]: plt.plot(r * np.cos(alphas), r * np.sin(alphas)) for barrier in barriers: plt.plot(barrier[:, 0], barrier[:, 1]) diff --git a/tests/test_design.py b/tests/test_design.py index bbc9d36..c1e27e9 100644 --- a/tests/test_design.py +++ b/tests/test_design.py @@ -2,28 +2,30 @@ import pytest -from machine_design.design import Design from machine_design.design_computation import Computation from machine_design.design_geometry import Geometry @pytest.fixture -def design(): - geometry = Geometry() - computation = Computation(geometry) - return Design(m2d=None, geometry=geometry, computation=computation) +def geometry(): + return Geometry() -def test_setup_name(design): - assert design.setup_name == "Setup1" +@pytest.fixture +def computation(geometry): + return Computation(geometry) + + +def test_setup_name(computation): + assert computation.setup_name == "Setup1" -def test_iron(design): - assert design.Fe == "Cogent Power - M350-50A, B-H at 50Hz" +def test_iron(geometry): + assert geometry.Fe == "Cogent Power - M350-50A, B-H at 50Hz" -def test_geom_params(design): - assert design.geom_params == { +def test_geom_params(geometry): + assert geometry.geom_params == { "DiaStatorGap": "79mm", "DiaStatorYoke": "125mm", "Airgap": "0.225mm", @@ -34,8 +36,8 @@ def test_geom_params(design): } -def test_slot_params(design): - assert design.slot_params == { +def test_slot_params(geometry): + assert geometry.slot_params == { "Hs0": "0.95mm", "Hs1": "0.31mm", "Hs2": "8.24mm", @@ -47,8 +49,8 @@ def test_slot_params(design): } -def test_wind_params(design): - assert design.wind_params == { +def test_wind_params(geometry): + assert geometry.wind_params == { "Layers": "1", "ParallelPaths": "1", "CoilPitch": "9", @@ -58,9 +60,9 @@ def test_wind_params(design): } -def test_mod_params(design): - assert design.PolePairs == 2 - assert design.mod_params == { +def test_mod_params(geometry): + assert geometry.PolePairs == 2 + assert geometry.mod_params == { "Poles": "2*2", "ModelLength": "85mm", "SymmetryFactor": "Poles", @@ -68,8 +70,8 @@ def test_mod_params(design): } -def test_oper_params(design): - assert design.oper_params == { +def test_oper_params(computation): + assert computation.oper_params == { "Im": "1.5*sqrt(2)A", "epsI": "pi/4", "InitPos": "-30deg", @@ -80,22 +82,22 @@ def test_oper_params(design): } -def test_rot_points(design): - assert len(design.rot_points) == 6 - assert all(len(point) == 3 for point in design.rot_points) +def test_rot_points(geometry): + assert len(geometry.rot_points) == 6 + assert all(len(point) == 3 for point in geometry.rot_points) -def test_derived_params(design): - assert design.rotor_r_min == pytest.approx(12.5) - assert design.rotor_r_max == pytest.approx(39.5 - 0.225) +def test_derived_params(geometry): + assert geometry.rotor_r_min == pytest.approx(12.5) + assert geometry.rotor_r_max == pytest.approx(39.5 - 0.225) -def test_solution_expressions(design): - assert design.solution_expressions == "Moving1.Torque" +def test_solution_expressions(computation): + assert computation.solution_expressions == "Moving1.Torque" -def test_udp_par_list_stator(design): - keys = [item[0] for item in design.udp_par_list_stator] +def test_udp_par_list_stator(geometry): + keys = [item[0] for item in geometry.udp_par_list_stator] assert keys == [ "DiaGap", "DiaYoke", @@ -119,31 +121,31 @@ def test_udp_par_list_stator(design): ] -def test_output_vars_only_reference_earlier_keys(design): +def test_output_vars_only_reference_earlier_keys(computation): # Ansys evaluates output variables in insertion order, so a formula referencing # another output variable must not reference one that is defined later. defined = set() - all_keys = set(design.output_vars.keys()) - for key, formula in design.output_vars.items(): + all_keys = set(computation.output_vars.keys()) + for key, formula in computation.output_vars.items(): later_keys = all_keys - defined - {key} referenced_later = [k for k in later_keys if re.search(rf"\b{re.escape(k)}\b", formula)] assert not referenced_later, f"'{key}' formula references not-yet-defined {referenced_later}" defined.add(key) -def test_output_vars_known_formula(design): - assert design.output_vars["Irms"] == "sqrt(I_d^2+I_q^2)/sqrt(2)" +def test_output_vars_known_formula(computation): + assert computation.output_vars["Irms"] == "sqrt(I_d^2+I_q^2)/sqrt(2)" -def test_post_params_plot_names_unique(design): - plot_names = list(design.post_params.values()) +def test_post_params_plot_names_unique(computation): + plot_names = list(computation.post_params.values()) assert len(plot_names) == len(set(plot_names)) -def test_mm_to_str(design): - assert design.mm_to_str("geom_params", "DiaShaft") == pytest.approx(25.0) +def test_mm_to_str(geometry): + assert geometry.mm_to_str("geom_params", "DiaShaft") == pytest.approx(25.0) -def test_mm_to_str_raises_without_mm_suffix(design): +def test_mm_to_str_raises_without_mm_suffix(geometry): with pytest.raises(Exception): - design.mm_to_str("geom_params", "SlotNumber") + geometry.mm_to_str("geom_params", "SlotNumber") diff --git a/tests/test_design2.py b/tests/test_design2.py index 99ac735..490211d 100644 --- a/tests/test_design2.py +++ b/tests/test_design2.py @@ -1,46 +1,46 @@ import pytest -from machine_design.design import Design -from machine_design.design_computation import Computation, Computation2 +from machine_design.design_computation import Computation2 from machine_design.design_geometry import Geometry, Geometry2 @pytest.fixture -def base(): - geometry = Geometry() - computation = Computation(geometry) - return Design(m2d=None, geometry=geometry, computation=computation) +def base_geometry(): + return Geometry() @pytest.fixture -def design2(): - geometry = Geometry2() - computation = Computation2(geometry) - return Design(m2d=None, geometry=geometry, computation=computation) +def geometry2(): + return Geometry2() -def test_geom_params_overrides_slot_number_only(base, design2): - expected = dict(base.geom_params) +@pytest.fixture +def computation2(geometry2): + return Computation2(geometry2) + + +def test_geom_params_overrides_slot_number_only(base_geometry, geometry2): + expected = dict(base_geometry.geom_params) expected["SlotNumber"] = "40" - assert design2.geom_params == expected + assert geometry2.geom_params == expected -def test_slot_params_overrides_bs1_bs2_setangle_only(base, design2): - expected = dict(base.slot_params) +def test_slot_params_overrides_bs1_bs2_setangle_only(base_geometry, geometry2): + expected = dict(base_geometry.slot_params) expected["Bs1"] = "3.0mm" expected["Bs2"] = "4.3mm" expected["SetAngle"] = "9deg" - assert design2.slot_params == expected + assert geometry2.slot_params == expected -def test_wind_params_overrides_nc_only(base, design2): - expected = dict(base.wind_params) +def test_wind_params_overrides_nc_only(base_geometry, geometry2): + expected = dict(base_geometry.wind_params) expected["Nc"] = "113" - assert design2.wind_params == expected + assert geometry2.wind_params == expected -def test_oper_params_fully_replaced(design2): - assert design2.oper_params == { +def test_oper_params_fully_replaced(computation2): + assert computation2.oper_params == { "Id1": "0.0A", "Iq1": "0.0A", "Id3": "0.0A", @@ -57,23 +57,23 @@ def test_oper_params_fully_replaced(design2): } -def test_derived_params_are_not_set(design2): +def test_derived_params_are_not_set(geometry2): # Geometry2.set_derived_params() is a no-op, unlike Geometry's. - assert not hasattr(design2, "rotor_r_min") - assert not hasattr(design2, "rotor_r_max") + assert not hasattr(geometry2, "rotor_r_min") + assert not hasattr(geometry2, "rotor_r_max") -def test_solution_expressions_is_a_list_ending_in_torque(design2): - assert isinstance(design2.solution_expressions, list) - assert design2.solution_expressions[-1] == "Moving1.Torque" - assert len(design2.solution_expressions) == 23 +def test_solution_expressions_is_a_list_ending_in_torque(computation2): + assert isinstance(computation2.solution_expressions, list) + assert computation2.solution_expressions[-1] == "Moving1.Torque" + assert len(computation2.solution_expressions) == 23 -def test_post_params_plot_names_unique(design2): - plot_names = list(design2.post_params.values()) +def test_post_params_plot_names_unique(computation2): + plot_names = list(computation2.post_params.values()) assert len(plot_names) == len(set(plot_names)) -def test_output_vars_known_values(design2): - assert design2.output_vars["Rstat"] == "19" - assert design2.output_vars["Lew"] == "0" +def test_output_vars_known_values(computation2): + assert computation2.output_vars["Rstat"] == "19" + assert computation2.output_vars["Lew"] == "0" From d6f1be3c9f78c266fedfb6ee4bffecfe33be4c05 Mon Sep 17 00:00:00 2001 From: sadda Date: Tue, 14 Jul 2026 08:48:49 +0000 Subject: [PATCH 18/29] Refactor --- machine_design/design_computation.py | 422 --------------------------- machine_design/design_geometry.py | 180 ------------ motors/__init__.py | 0 motors/motor1.py | 266 +++++++++++++++++ motors/motor2.py | 346 ++++++++++++++++++++++ pyproject.toml | 1 + tests/test_ansys_regression.py | 3 +- tests/test_design.py | 3 +- tests/test_design2.py | 4 +- 9 files changed, 617 insertions(+), 608 deletions(-) create mode 100644 motors/__init__.py create mode 100644 motors/motor1.py create mode 100644 motors/motor2.py diff --git a/machine_design/design_computation.py b/machine_design/design_computation.py index 2b6bd5f..d2248fc 100644 --- a/machine_design/design_computation.py +++ b/machine_design/design_computation.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod -import numpy as np from ansys.aedt.core import Maxwell2d from .design_geometry import GeometryBase @@ -112,424 +111,3 @@ def compute(self, m2d: Maxwell2d, rotor_id, *args, NUM_CORES: int = 1): m2d.odesign.DeleteFullVariation("All", False) return result - - -class Computation(ComputationBase): - def set_oper_params(self): - f = 50 # [Hz] - RotSpeed = 60 * f / self.geometry.PolePairs # [rpm] - self.oper_params = { - "Im": "1.5*sqrt(2)A", - "epsI": "pi/4", # current angle - "InitPos": "-30deg", - "f": f"{f}Hz", - "RotSpeed": f"{RotSpeed}rpm", - "Nper": "1/6", # number of included periods - "PointPer": "101", # number of time points per period - } - - def set_solution_expressions(self): - self.solution_expressions = "Moving1.Torque" - - def set_output_vars(self): - self.output_vars = { - "pos": "(Moving1.Position -InitPos) * Poles/2", - "cos0": "cos(pos)", - "cos1": "cos(pos-2*PI/3)", - "cos2": "cos(pos-4*PI/3)", - "sin0": "sin(pos)", - "sin1": "sin(pos-2*PI/3)", - "sin2": "sin(pos-4*PI/3)", - "Lad": "L(PhaseA,PhaseA)*cos0 + L(PhaseA,PhaseB)*cos1 + L(PhaseA,PhaseC)*cos2", - "Laq": "L(PhaseA,PhaseA)*sin0 + L(PhaseA,PhaseB)*sin1 + L(PhaseA,PhaseC)*sin2", - "Lbd": "L(PhaseB,PhaseA)*cos0 + L(PhaseB,PhaseB)*cos1 + L(PhaseB,PhaseC)*cos2", - "Lbq": "L(PhaseB,PhaseA)*sin0 + L(PhaseB,PhaseB)*sin1 + L(PhaseB,PhaseC)*sin2", - "Lcd": "L(PhaseC,PhaseA)*cos0 + L(PhaseC,PhaseB)*cos1 + L(PhaseC,PhaseC)*cos2", - "Lcq": "L(PhaseC,PhaseA)*sin0 + L(PhaseC,PhaseB)*sin1 + L(PhaseC,PhaseC)*sin2", - "L_d": "(Lad*cos0 + Lbd*cos1 + Lcd*cos2) * 2/3", - "L_q": "(Laq*sin0 + Lbq*sin1 + Lcq*sin2) * 2/3", - "Flux_d": "(FluxLinkage(PhaseA)*cos0+FluxLinkage(PhaseB)*cos1+FluxLinkage(PhaseC)*cos2)*2/3", - "Flux_q": "-(FluxLinkage(PhaseA)*sin0+FluxLinkage(PhaseB)*sin1+FluxLinkage(PhaseC)*sin2)*2/3", - "Ui_d": "(InducedVoltage(PhaseA)*cos0+InducedVoltage(PhaseB)*cos1+InducedVoltage(PhaseC)*cos2)*2/3", - "Ui_q": "-(InducedVoltage(PhaseA)*sin0+InducedVoltage(PhaseB)*sin1+InducedVoltage(PhaseC)*sin2)*2/3", - "I_d": "(InputCurrent(PhaseA)*cos0 + InputCurrent(PhaseB)*cos1 + InputCurrent(PhaseC)*cos2)*2/3", - "I_q": "-(InputCurrent(PhaseA)*sin0 + InputCurrent(PhaseB)*sin1 + InputCurrent(PhaseC)*sin2)*2/3", - "Irms": "sqrt(I_d^2+I_q^2)/sqrt(2)", - } - - def set_post_params(self): - self.post_params = { # reports - ("InducedVoltage(PhaseA)", "InducedVoltage(PhaseB)", "InducedVoltage(PhaseC)"): "InducedVoltage", - ("Moving1.Torque"): "Torque", - ("InputCurrent(PhaseA)", "InputCurrent(PhaseB)", "InputCurrent(PhaseC)"): "Current", - ( - "FluxLinkage(PhaseA)", - "FluxLinkage(PhaseB)", - "FluxLinkage(PhaseC)", - ): "FluxLinkage", - ("I_d", "I_q"): "Current_dq", - ("Flux_d", "Flux_q"): "FluxLinkage_dq", - ("Ui_d", "Ui_q"): "InducedVoltage_dq", - ("L_d", "L_q"): "Inductance_dq", - } - - def assign_stator_coils(self, m2d: Maxwell2d) -> None: - # Excitations - I_A = "Im * cos(2*pi*f*time+epsI)" - I_B = "Im * cos(2*pi*f*time-120deg+epsI)" - I_C = "Im * cos(2*pi*f*time-240deg+epsI)" - - phases_polarity = ["Positive", "Negative", "Positive"] - phases_name = ["A", "C", "B"] - phases_current = [I_A, I_C, I_B] - i_coil = 0 - for phase_polarity, phase_name, phase_current in zip(phases_polarity, phases_name, phases_current): - names = [] - for _ in range(3): - m2d.assign_coil( - assignment=[self.geometry.id_coils[i_coil]], - conductors_number="Nc", - polarity=phase_polarity, - name=f"CS{i_coil+1}", - ) - names.append(f"CS{i_coil+1}") - i_coil += 1 - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=phase_current, - parallel_branches="ParallelPaths", - name=f"Phase{phase_name}", - ) - m2d.add_winding_coils(assignment=f"Phase{phase_name}", coils=names) - - def inductance_computation(self, m2d: Maxwell2d) -> None: - m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=False) - - def set_variables(self, m2d: Maxwell2d, *args): - pass - - def extract_results(self, solutions): - return solutions.data_magnitude() - - -class Computation2(ComputationBase): - def set_oper_params(self): - f = 50 # [Hz] - RotSpeed = 60 * f / self.geometry.PolePairs # [rpm] - self.oper_params = { - "Id1": "0.0A", - "Iq1": "0.0A", - "Id3": "0.0A", - "Iq3": "0.0A", - "epsI1": "atan2(Iq1,Id1)", # current angle, 1st harmonic - "epsI3": "atan2(Iq3,Id3)", # current angle, 1st harmonic - "Im1": "sqrt(Id1^2+Iq1^2)", - "Im3": "sqrt(Id3^2+Iq3^2)", - "InitPos": "-45deg", - "f": f"{f}Hz", - "RotSpeed": f"{RotSpeed}rpm", - "Nper": "1/10", # number of included periods - "PointPer": "101", # number of time points per period - } - - def set_solution_expressions(self): - self.solution_expressions = [ - "V_d1", - "V_q1", - "V_d3", - "V_q3", - "Flux_e_d1", - "Flux_e_q1", - "Flux_e_d3", - "Flux_e_q3", - "I_d1", - "I_q1", - "I_d3", - "I_q3", - "Ld1", - "Ld1q1", - "Ld1d3", - "Ld1q3", - "Lq1", - "Lq1d3", - "Lq1q3", - "Ld3", - "Ld3q3", - "Lq3", - "Moving1.Torque", - ] - - def set_output_vars(self): - self.output_vars = { - "PolePairs": "2", - "RotSign": "1", - "Rstat": "19", - "Lew": "0", - "theta_el": "RotSign*(Moving1.Position - InitPos) * PolePairs - pi", - "cos0_1": "cos(1*(theta_el - 2*PI*0/5))", - "sin0_1": "sin(-1*(theta_el - 2*PI*0/5))", - "cos1_1": "cos(1*(theta_el - 2*PI*1/5))", - "sin1_1": "sin(-1*(theta_el - 2*PI*1/5))", - "cos2_1": "cos(1*(theta_el - 2*PI*2/5))", - "sin2_1": "sin(-1*(theta_el - 2*PI*2/5))", - "cos3_1": "cos(1*(theta_el - 2*PI*3/5))", - "sin3_1": "sin(-1*(theta_el - 2*PI*3/5))", - "cos4_1": "cos(1*(theta_el - 2*PI*4/5))", - "sin4_1": "sin(-1*(theta_el - 2*PI*4/5))", - "cos0_3": "cos(3*(theta_el - 2*PI*0/5))", - "sin0_3": "sin(-3*(theta_el - 2*PI*0/5))", - "cos1_3": "cos(3*(theta_el - 2*PI*1/5))", - "sin1_3": "sin(-3*(theta_el - 2*PI*1/5))", - "cos2_3": "cos(3*(theta_el - 2*PI*2/5))", - "sin2_3": "sin(-3*(theta_el - 2*PI*2/5))", - "cos3_3": "cos(3*(theta_el - 2*PI*3/5))", - "sin3_3": "sin(-3*(theta_el - 2*PI*3/5))", - "cos4_3": "cos(3*(theta_el - 2*PI*4/5))", - "sin4_3": "sin(-3*(theta_el - 2*PI*4/5))", - "Flux_d1": "(FluxLinkage(PhaseA)*cos0_1 + FluxLinkage(PhaseB)*cos1_1 + FluxLinkage(PhaseC)*cos2_1 + FluxLinkage(PhaseD)*cos3_1 + FluxLinkage(PhaseE)*cos4_1) * 2/5", - "Flux_q1": "(FluxLinkage(PhaseA)*sin0_1 + FluxLinkage(PhaseB)*sin1_1 + FluxLinkage(PhaseC)*sin2_1 + FluxLinkage(PhaseD)*sin3_1 + FluxLinkage(PhaseE)*sin4_1) * 2/5", - "Flux_d3": "(FluxLinkage(PhaseA)*cos0_3 + FluxLinkage(PhaseB)*cos1_3 + FluxLinkage(PhaseC)*cos2_3 + FluxLinkage(PhaseD)*cos3_3 + FluxLinkage(PhaseE)*cos4_3) * 2/5", - "Flux_q3": "(FluxLinkage(PhaseA)*sin0_3 + FluxLinkage(PhaseB)*sin1_3 + FluxLinkage(PhaseC)*sin2_3 + FluxLinkage(PhaseD)*sin3_3 + FluxLinkage(PhaseE)*sin4_3) * 2/5", - "Vind_d1": "(InducedVoltage(PhaseA)*cos0_1 + InducedVoltage(PhaseB)*cos1_1 + InducedVoltage(PhaseC)*cos2_1 + InducedVoltage(PhaseD)*cos3_1 + InducedVoltage(PhaseE)*cos4_1) * 2/5", - "Vind_q1": "(InducedVoltage(PhaseA)*sin0_1 + InducedVoltage(PhaseB)*sin1_1 + InducedVoltage(PhaseC)*sin2_1 + InducedVoltage(PhaseD)*sin3_1 + InducedVoltage(PhaseE)*sin4_1) * 2/5", - "Vind_d3": "(InducedVoltage(PhaseA)*cos0_3 + InducedVoltage(PhaseB)*cos1_3 + InducedVoltage(PhaseC)*cos2_3 + InducedVoltage(PhaseD)*cos3_3 + InducedVoltage(PhaseE)*cos4_3) * 2/5", - "Vind_q3": "(InducedVoltage(PhaseA)*sin0_3 + InducedVoltage(PhaseB)*sin1_3 + InducedVoltage(PhaseC)*sin2_3 + InducedVoltage(PhaseD)*sin3_3 + InducedVoltage(PhaseE)*sin4_3) * 2/5", - "V_A": "InducedVoltage(PhaseA) + Rstat*InputCurrent(PhaseA) + Lew*deriv(InputCurrent(PhaseA))", - "V_B": "InducedVoltage(PhaseB) + Rstat*InputCurrent(PhaseB) + Lew*deriv(InputCurrent(PhaseB))", - "V_C": "InducedVoltage(PhaseC) + Rstat*InputCurrent(PhaseC) + Lew*deriv(InputCurrent(PhaseC))", - "V_D": "InducedVoltage(PhaseD) + Rstat*InputCurrent(PhaseD) + Lew*deriv(InputCurrent(PhaseD))", - "V_E": "InducedVoltage(PhaseE) + Rstat*InputCurrent(PhaseE) + Lew*deriv(InputCurrent(PhaseE))", - "V_AC": "V_A - V_C", - "V_BD": "V_B - V_D", - "V_CE": "V_C - V_E", - "V_DA": "V_D - V_A", - "V_EB": "V_E - V_B", - "Vterm_A": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", - "Vterm_B": "1/5*(2*V_AC + 4*V_BD + 1*V_CE + 3*V_DA)", - "Vterm_C": "1/5*(-3*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", - "Vterm_D": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + 3*V_DA)", - "Vterm_E": "1/5*(-3*V_AC + -1*V_BD + -4*V_CE + -2*V_DA)", - "I_d1": "(InputCurrent(PhaseA)*cos0_1 + InputCurrent(PhaseB)*cos1_1 + InputCurrent(PhaseC)*cos2_1 + InputCurrent(PhaseD)*cos3_1 + InputCurrent(PhaseE)*cos4_1) * 2/5", - "I_q1": "(InputCurrent(PhaseA)*sin0_1 + InputCurrent(PhaseB)*sin1_1 + InputCurrent(PhaseC)*sin2_1 + InputCurrent(PhaseD)*sin3_1 + InputCurrent(PhaseE)*sin4_1) * 2/5", - "I_d3": "(InputCurrent(PhaseA)*cos0_3 + InputCurrent(PhaseB)*cos1_3 + InputCurrent(PhaseC)*cos2_3 + InputCurrent(PhaseD)*cos3_3 + InputCurrent(PhaseE)*cos4_3) * 2/5", - "I_q3": "(InputCurrent(PhaseA)*sin0_3 + InputCurrent(PhaseB)*sin1_3 + InputCurrent(PhaseC)*sin2_3 + InputCurrent(PhaseD)*sin3_3 + InputCurrent(PhaseE)*sin4_3) * 2/5", - "V_d1": "(V_A*cos0_1 + V_B*cos1_1 + V_C*cos2_1 + V_D*cos3_1 + V_E*cos4_1) * 2/5", - "V_q1": "(V_A*sin0_1 + V_B*sin1_1 + V_C*sin2_1 + V_D*sin3_1 + V_E*sin4_1) * 2/5", - "V_d3": "(V_A*cos0_3 + V_B*cos1_3 + V_C*cos2_3 + V_D*cos3_3 + V_E*cos4_3) * 2/5", - "V_q3": "(V_A*sin0_3 + V_B*sin1_3 + V_C*sin2_3 + V_D*sin3_3 + V_E*sin4_3) * 2/5", - "L0d_1": "L(PhaseA,PhaseA)*cos0_1 + L(PhaseA,PhaseB)*cos1_1 + L(PhaseA,PhaseC)*cos2_1 + L(PhaseA,PhaseD)*cos3_1 + L(PhaseA,PhaseE)*cos4_1", - "L0q_1": "L(PhaseA,PhaseA)*sin0_1 + L(PhaseA,PhaseB)*sin1_1 + L(PhaseA,PhaseC)*sin2_1 + L(PhaseA,PhaseD)*sin3_1 + L(PhaseA,PhaseE)*sin4_1", - "L1d_1": "L(PhaseB,PhaseA)*cos0_1 + L(PhaseB,PhaseB)*cos1_1 + L(PhaseB,PhaseC)*cos2_1 + L(PhaseB,PhaseD)*cos3_1 + L(PhaseB,PhaseE)*cos4_1", - "L1q_1": "L(PhaseB,PhaseA)*sin0_1 + L(PhaseB,PhaseB)*sin1_1 + L(PhaseB,PhaseC)*sin2_1 + L(PhaseB,PhaseD)*sin3_1 + L(PhaseB,PhaseE)*sin4_1", - "L2d_1": "L(PhaseC,PhaseA)*cos0_1 + L(PhaseC,PhaseB)*cos1_1 + L(PhaseC,PhaseC)*cos2_1 + L(PhaseC,PhaseD)*cos3_1 + L(PhaseC,PhaseE)*cos4_1", - "L2q_1": "L(PhaseC,PhaseA)*sin0_1 + L(PhaseC,PhaseB)*sin1_1 + L(PhaseC,PhaseC)*sin2_1 + L(PhaseC,PhaseD)*sin3_1 + L(PhaseC,PhaseE)*sin4_1", - "L3d_1": "L(PhaseD,PhaseA)*cos0_1 + L(PhaseD,PhaseB)*cos1_1 + L(PhaseD,PhaseC)*cos2_1 + L(PhaseD,PhaseD)*cos3_1 + L(PhaseD,PhaseE)*cos4_1", - "L3q_1": "L(PhaseD,PhaseA)*sin0_1 + L(PhaseD,PhaseB)*sin1_1 + L(PhaseD,PhaseC)*sin2_1 + L(PhaseD,PhaseD)*sin3_1 + L(PhaseD,PhaseE)*sin4_1", - "L4d_1": "L(PhaseE,PhaseA)*cos0_1 + L(PhaseE,PhaseB)*cos1_1 + L(PhaseE,PhaseC)*cos2_1 + L(PhaseE,PhaseD)*cos3_1 + L(PhaseE,PhaseE)*cos4_1", - "L4q_1": "L(PhaseE,PhaseA)*sin0_1 + L(PhaseE,PhaseB)*sin1_1 + L(PhaseE,PhaseC)*sin2_1 + L(PhaseE,PhaseD)*sin3_1 + L(PhaseE,PhaseE)*sin4_1", - "L0d_3": "L(PhaseA,PhaseA)*cos0_3 + L(PhaseA,PhaseB)*cos1_3 + L(PhaseA,PhaseC)*cos2_3 + L(PhaseA,PhaseD)*cos3_3 + L(PhaseA,PhaseE)*cos4_3", - "L0q_3": "L(PhaseA,PhaseA)*sin0_3 + L(PhaseA,PhaseB)*sin1_3 + L(PhaseA,PhaseC)*sin2_3 + L(PhaseA,PhaseD)*sin3_3 + L(PhaseA,PhaseE)*sin4_3", - "L1d_3": "L(PhaseB,PhaseA)*cos0_3 + L(PhaseB,PhaseB)*cos1_3 + L(PhaseB,PhaseC)*cos2_3 + L(PhaseB,PhaseD)*cos3_3 + L(PhaseB,PhaseE)*cos4_3", - "L1q_3": "L(PhaseB,PhaseA)*sin0_3 + L(PhaseB,PhaseB)*sin1_3 + L(PhaseB,PhaseC)*sin2_3 + L(PhaseB,PhaseD)*sin3_3 + L(PhaseB,PhaseE)*sin4_3", - "L2d_3": "L(PhaseC,PhaseA)*cos0_3 + L(PhaseC,PhaseB)*cos1_3 + L(PhaseC,PhaseC)*cos2_3 + L(PhaseC,PhaseD)*cos3_3 + L(PhaseC,PhaseE)*cos4_3", - "L2q_3": "L(PhaseC,PhaseA)*sin0_3 + L(PhaseC,PhaseB)*sin1_3 + L(PhaseC,PhaseC)*sin2_3 + L(PhaseC,PhaseD)*sin3_3 + L(PhaseC,PhaseE)*sin4_3", - "L3d_3": "L(PhaseD,PhaseA)*cos0_3 + L(PhaseD,PhaseB)*cos1_3 + L(PhaseD,PhaseC)*cos2_3 + L(PhaseD,PhaseD)*cos3_3 + L(PhaseD,PhaseE)*cos4_3", - "L3q_3": "L(PhaseD,PhaseA)*sin0_3 + L(PhaseD,PhaseB)*sin1_3 + L(PhaseD,PhaseC)*sin2_3 + L(PhaseD,PhaseD)*sin3_3 + L(PhaseD,PhaseE)*sin4_3", - "L4d_3": "L(PhaseE,PhaseA)*cos0_3 + L(PhaseE,PhaseB)*cos1_3 + L(PhaseE,PhaseC)*cos2_3 + L(PhaseE,PhaseD)*cos3_3 + L(PhaseE,PhaseE)*cos4_3", - "L4q_3": "L(PhaseE,PhaseA)*sin0_3 + L(PhaseE,PhaseB)*sin1_3 + L(PhaseE,PhaseC)*sin2_3 + L(PhaseE,PhaseD)*sin3_3 + L(PhaseE,PhaseE)*sin4_3", - "Ld1": "(L0d_1*cos0_1 + L1d_1*cos1_1 + L2d_1*cos2_1 + L3d_1*cos3_1 + L4d_1*cos4_1) * 2/5", - "Ld1q1": "(L0d_1*sin0_1 + L1d_1*sin1_1 + L2d_1*sin2_1 + L3d_1*sin3_1 + L4d_1*sin4_1) * 2/5", - "Lq1d1": "(L0q_1*cos0_1 + L1q_1*cos1_1 + L2q_1*cos2_1 + L3q_1*cos3_1 + L4q_1*cos4_1) * 2/5", - "Lq1": "(L0q_1*sin0_1 + L1q_1*sin1_1 + L2q_1*sin2_1 + L3q_1*sin3_1 + L4q_1*sin4_1) * 2/5", - "Ld1d3": "(L0d_1*cos0_3 + L1d_1*cos1_3 + L2d_1*cos2_3 + L3d_1*cos3_3 + L4d_1*cos4_3) * 2/5", - "Ld1q3": "(L0d_1*sin0_3 + L1d_1*sin1_3 + L2d_1*sin2_3 + L3d_1*sin3_3 + L4d_1*sin4_3) * 2/5", - "Lq1d3": "(L0q_1*cos0_3 + L1q_1*cos1_3 + L2q_1*cos2_3 + L3q_1*cos3_3 + L4q_1*cos4_3) * 2/5", - "Lq1q3": "(L0q_1*sin0_3 + L1q_1*sin1_3 + L2q_1*sin2_3 + L3q_1*sin3_3 + L4q_1*sin4_3) * 2/5", - "Ld3d1": "(L0d_3*cos0_1 + L1d_3*cos1_1 + L2d_3*cos2_1 + L3d_3*cos3_1 + L4d_3*cos4_1) * 2/5", - "Ld3q1": "(L0d_3*sin0_1 + L1d_3*sin1_1 + L2d_3*sin2_1 + L3d_3*sin3_1 + L4d_3*sin4_1) * 2/5", - "Lq3d1": "(L0q_3*cos0_1 + L1q_3*cos1_1 + L2q_3*cos2_1 + L3q_3*cos3_1 + L4q_3*cos4_1) * 2/5", - "Lq3q1": "(L0q_3*sin0_1 + L1q_3*sin1_1 + L2q_3*sin2_1 + L3q_3*sin3_1 + L4q_3*sin4_1) * 2/5", - "Ld3": "(L0d_3*cos0_3 + L1d_3*cos1_3 + L2d_3*cos2_3 + L3d_3*cos3_3 + L4d_3*cos4_3) * 2/5", - "Ld3q3": "(L0d_3*sin0_3 + L1d_3*sin1_3 + L2d_3*sin2_3 + L3d_3*sin3_3 + L4d_3*sin4_3) * 2/5", - "Lq3d3": "(L0q_3*cos0_3 + L1q_3*cos1_3 + L2q_3*cos2_3 + L3q_3*cos3_3 + L4q_3*cos4_3) * 2/5", - "Lq3": "(L0q_3*sin0_3 + L1q_3*sin1_3 + L2q_3*sin2_3 + L3q_3*sin3_3 + L4q_3*sin4_3) * 2/5", - "Flux_e_d1": "Flux_d1 - (Ld1*I_d1 + Ld1q1*I_q1 + Ld1d3*I_d3 + Ld1q3*I_q3)", - "Flux_e_q1": "Flux_q1 - (Lq1d1*I_d1 + Lq1*I_q1 + Lq1d3*I_d3 + Lq1q3*I_q3)", - "Flux_e_d3": "Flux_d3 - (Ld3d1*I_d1 + Ld3q1*I_q1 + Ld3*I_d3 + Ld3q3*I_q3)", - "Flux_e_q3": "Flux_q3 - (Lq3d1*I_d1 + Lq3q1*I_q1 + Lq3d3*I_d3 + Lq3*I_q3)", - "Torque_dq": "5/2*PolePairs*(1*(Flux_d1*I_q1 - Flux_q1*I_d1) + 3*(Flux_d3*I_q3 - Flux_q3*I_d3))", - } - - def set_post_params(self): - self.post_params = { # reports - ( - "InducedVoltage(PhaseA)", - "InducedVoltage(PhaseB)", - "InducedVoltage(PhaseC)", - "InducedVoltage(PhaseD)", - "InducedVoltage(PhaseE)", - ): "InducedVoltage", - ("Moving1.Torque"): "Torque", - ( - "InputCurrent(PhaseA)", - "InputCurrent(PhaseB)", - "InputCurrent(PhaseC)", - "InputCurrent(PhaseD)", - "InputCurrent(PhaseE)", - ): "Current", - ( - "FluxLinkage(PhaseA)", - "FluxLinkage(PhaseB)", - "FluxLinkage(PhaseC)", - "FluxLinkage(PhaseD)", - "FluxLinkage(PhaseE)", - ): "FluxLinkage", - ("I_d1", "I_q1", "I_d3", "I_q3"): "Current_dq", - ("Flux_d1", "Flux_q1", "Flux_d3", "Flux_q3"): "FluxLinkage_dq", - ("Flux_e_d1", "Flux_e_q1", "Flux_e_d3", "Flux_e_q3"): "FluxLinkage excitation_dq", - ("Vind_d1", "Vind_q1", "Vind_d3", "Vind_q3"): "InducedVoltage_dq", - ("V_d1", "V_q1", "V_d3", "V_q3"): "TerminalVoltage_dq", - ("Ld1", "Lq1", "Ld3", "Lq3"): "Inductance_dq main", - ("Ld1q1", "Ld1d3", "Ld1q3", "Lq1d3", "Lq1q3", "Ld3q3"): "Inductance_dq cross-coupling", - } - - def assign_stator_coils(self, m2d: Maxwell2d) -> None: - # Excitations - I_A = "Im1*cos(2*pi*f*time+epsI1-pi) + Im3*cos(3*(2*pi*f*time)+epsI3-pi)" - I_B = "Im1*cos(2*pi*f*time-72deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-72deg)+epsI3-pi)" - I_C = "Im1*cos(2*pi*f*time-144deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-144deg)+epsI3-pi)" - I_D = "Im1*cos(2*pi*f*time-216deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-216deg)+epsI3-pi)" - I_E = "Im1*cos(2*pi*f*time-288deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-288deg)+epsI3-pi)" - m2d.assign_coil - # Define phase windings - m2d.assign_coil( - assignment=["Coil"], - conductors_number="Nc", - polarity="Positive", - name="CS1", - ) - m2d.assign_coil( - assignment=["Coil_1"], - conductors_number="Nc", - polarity="Negative", - name="CS2", - ) - m2d.assign_coil( - assignment=["Coil_2"], - conductors_number="Nc", - polarity="Negative", - name="CS3", - ) - m2d.assign_coil( - assignment=["Coil_3"], - conductors_number="Nc", - polarity="Positive", - name="CS4", - ) - m2d.assign_coil( - assignment=["Coil_4"], - conductors_number="Nc", - polarity="Positive", - name="CS5", - ) - m2d.assign_coil( - assignment=["Coil_5"], - conductors_number="Nc", - polarity="Negative", - name="CS6", - ) - m2d.assign_coil( - assignment=["Coil_6"], - conductors_number="Nc", - polarity="Negative", - name="CS7", - ) - m2d.assign_coil( - assignment=["Coil_7"], - conductors_number="Nc", - polarity="Positive", - name="CS8", - ) - m2d.assign_coil( - assignment=["Coil_8"], - conductors_number="Nc", - polarity="Positive", - name="CS9", - ) - m2d.assign_coil( - assignment=["Coil_9"], - conductors_number="Nc", - polarity="Negative", - name="CS10", - ) - - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_A, - parallel_branches="ParallelPaths", - name="PhaseA", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_B, - parallel_branches="ParallelPaths", - name="PhaseB", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_C, - parallel_branches="ParallelPaths", - name="PhaseC", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_D, - parallel_branches="ParallelPaths", - name="PhaseD", - ) - m2d.assign_winding( - assignment=None, - winding_type="Current", - is_solid=False, - current=I_E, - parallel_branches="ParallelPaths", - name="PhaseE", - ) - - m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS10"]) - m2d.add_winding_coils(assignment="PhaseB", coils=["CS4", "CS5"]) - m2d.add_winding_coils(assignment="PhaseC", coils=["CS8", "CS9"]) - m2d.add_winding_coils(assignment="PhaseD", coils=["CS2", "CS3"]) - m2d.add_winding_coils(assignment="PhaseE", coils=["CS6", "CS7"]) - - def inductance_computation(self, m2d: Maxwell2d) -> None: - m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=True) - - def set_variables(self, m2d: Maxwell2d, Id1, Iq1, Id3, Iq3): - m2d.variable_manager["Id1"] = f"{Id1}A" - m2d.variable_manager["Iq1"] = f"{Iq1}A" - m2d.variable_manager["Id3"] = f"{Id3}A" - m2d.variable_manager["Iq3"] = f"{Iq3}A" - - def extract_results(self, solutions): - # TODO: this works only because torque is the last one in the array - out = np.zeros(len(self.solution_expressions)) - for i, expr in enumerate(self.solution_expressions): - data = solutions.data_real(expr) - val = float(np.mean(data[:-1])) - - if expr.startswith("L_"): - val /= 1e9 - - out[i] = val - return data diff --git a/machine_design/design_geometry.py b/machine_design/design_geometry.py index 2b511ed..58d9dc2 100644 --- a/machine_design/design_geometry.py +++ b/machine_design/design_geometry.py @@ -190,183 +190,3 @@ def _assign_boundary_conditions(self, m2d: Maxwell2d) -> None: assignment="Region", ) m2d.assign_vector_potential(assignment=id_bc_az, vector_value=0, boundary="A0") - - -class Geometry(GeometryBase): - def set_iron(self): - self.Fe = "Cogent Power - M350-50A, B-H at 50Hz" - - def set_geom_params(self): - self.geom_params = { - "DiaStatorGap": "79mm", - "DiaStatorYoke": "125mm", - "Airgap": "0.225mm", - "SlotNumber": "36", - "SlotType": "3", - "DiaShaft": "25mm", - "StackLength": "85mm", - } - - def set_slot_params(self): - self.slot_params = { - "Hs0": "0.95mm", - "Hs1": "0.31mm", - "Hs2": "8.24mm", - "Bs0": "2.2mm", - "Bs1": "3.37mm", - "Bs2": "4.8mm", - "Rs": "1.5mm", - "SetAngle": "10deg", - } - - def set_winds_params(self): - self.wind_params = { - "Layers": "1", - "ParallelPaths": "1", - "CoilPitch": "9", # coil pitch in slots - "SlotLiner": "0.3mm", - "SpaceLayers": "0.2mm", - "Nc": "68", # turns per coil - } - - def set_mod_params(self): - self.PolePairs = 2 - self.mod_params = { - "Poles": f"2*{self.PolePairs}", - "ModelLength": "85mm", - "SymmetryFactor": "Poles", - "StatorSkewAngle": "0deg", - } - - def set_rot_points(self): - self.rot_points = [ - ["DiaShaft/2*cos(360deg/SymmetryFactor)", "DiaShaft/2*sin(360deg/SymmetryFactor)", "0mm"], - ["DiaShaft/2*cos(360deg/(2*SymmetryFactor))", "DiaShaft/2*sin(360deg/(2*SymmetryFactor))", "0mm"], - ["DiaShaft/2", "0mm", "0mm"], - ["DiaStatorGap/2-Airgap", "0mm", "0mm"], - ["(DiaStatorGap/2-Airgap)*cos(360deg/(2*SymmetryFactor))", "(DiaStatorGap/2-Airgap)*sin(360deg/(2*SymmetryFactor))", "0mm"], - ["(DiaStatorGap/2-Airgap)*cos(360deg/SymmetryFactor)", "(DiaStatorGap/2-Airgap)*sin(360deg/SymmetryFactor)", "0mm"], - ] - - def set_derived_params(self): - self.rotor_r_min = self.mm_to_str("geom_params", "DiaShaft") / 2 - self.rotor_r_max = self.mm_to_str("geom_params", "DiaStatorGap") / 2 - self.mm_to_str("geom_params", "Airgap") - - def set_udp_par_list_stator(self): - self.udp_par_list_stator = [ - ["DiaGap", "DiaStatorGap"], - ["DiaYoke", "DiaStatorYoke"], - ["Length", "0mm"], - ["Skew", "0deg"], - ["Slots", "SlotNumber"], - ["SlotType", "SlotType"], - ["Hs0", "Hs0"], - ["Hs01", "0mm"], - ["Hs1", "Hs1"], - ["Hs2", "Hs2"], - ["Bs0", "Bs0"], - ["Bs1", "Bs1"], - ["Bs2", "Bs2"], - ["Rs", "Rs"], - ["FilletType", "0"], - ["HalfSlot", "0"], - ["SegAngle", "0deg"], - ["LenRegion", "0mm"], - ["InfoCore", "0"], - ] - - def build_stator(self, m2d: Maxwell2d) -> None: - self._push_stator_variables(m2d) - shaft_id, region_id, band_id = self._build_vacuum_regions(m2d) - self._assign_rotor_motion(m2d) - stator_id = self._build_stator_core(m2d) - id_coils = self._build_stator_coils(m2d) - self._split_for_symmetry(m2d, [stator_id, shaft_id, region_id, band_id]) - self._assign_boundary_conditions(m2d) - self._assign_stator_mesh(m2d, stator_id, id_coils) - - # core loss - m2d.set_core_losses("Stator", core_loss_on_field=False) - - self.stator_id = stator_id - self.shaft_id = shaft_id - self.region_id = region_id - self.band_id = band_id - self.id_coils = id_coils - - def _assign_rotor_motion(self, m2d: Maxwell2d) -> None: - m2d.assign_rotate_motion( - assignment="Band", - coordinate_system="Global", - axis="Z", - positive_movement=True, - start_position="InitPos", - angular_velocity="RotSpeed", - has_rotation_limits=False, - ) - - def _build_stator_core(self, m2d: Maxwell2d): - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - stator_id = modeler.create_udp( - dll="RMxprt/SlotCore.dll", - parameters=self.udp_par_list_stator, - library="syslib", - name="Stator", - # SolveInside="True", - ) - stator_id.material_name = self.Fe - - return stator_id - - def _build_stator_coils(self, m2d: Maxwell2d): - modeler = m2d.modeler - assert isinstance(modeler, Modeler2D) - - coil_id = modeler.create_rectangle( - origin=["DiaStatorGap/2+Hs0+Hs1+SlotLiner", "-(Bs1/2-SlotLiner)", 0], - sizes=["(Hs2+Rs/2-2*SlotLiner)/Layers-(Layers-1)*SpaceLayers/2", "Bs1-2*SlotLiner", 0], - name="Coil", - material="copper", - ) - modeler.rotate(assignment=coil_id, axis="Z", angle="360deg/SlotNumber/2") - coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", clones="SlotNumber/Poles", create_new_objects=True) - id_coils = modeler.get_objects_w_string(string_name="Coil", case_sensitive=True) - - return id_coils - - def _assign_stator_mesh(self, m2d: Maxwell2d, stator_id, id_coils) -> None: - m2d.mesh.assign_length_mesh( - assignment=id_coils, - inside_selection=True, - maximum_length=3, - maximum_elements=None, - name="coils", - ) - m2d.mesh.assign_length_mesh( - assignment=stator_id, - inside_selection=True, - maximum_length=3, - maximum_elements=None, - name="stator", - ) - - -class Geometry2(Geometry): - def set_geom_params(self): - super().set_geom_params() - self.geom_params["SlotNumber"] = "40" - - def set_slot_params(self): - super().set_slot_params() - self.slot_params["Bs1"] = "3.0mm" - self.slot_params["Bs2"] = "4.3mm" - self.slot_params["SetAngle"] = "9deg" - - def set_winds_params(self): - super().set_winds_params() - self.wind_params["Nc"] = "113" - - def set_derived_params(self): - pass diff --git a/motors/__init__.py b/motors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/motors/motor1.py b/motors/motor1.py new file mode 100644 index 0000000..7466c4b --- /dev/null +++ b/motors/motor1.py @@ -0,0 +1,266 @@ +from ansys.aedt.core import Maxwell2d +from ansys.aedt.core.modeler.modeler_2d import Modeler2D + +from machine_design.design_computation import ComputationBase +from machine_design.design_geometry import GeometryBase + + +class Geometry(GeometryBase): + def set_iron(self): + self.Fe = "Cogent Power - M350-50A, B-H at 50Hz" + + def set_geom_params(self): + self.geom_params = { + "DiaStatorGap": "79mm", + "DiaStatorYoke": "125mm", + "Airgap": "0.225mm", + "SlotNumber": "36", + "SlotType": "3", + "DiaShaft": "25mm", + "StackLength": "85mm", + } + + def set_slot_params(self): + self.slot_params = { + "Hs0": "0.95mm", + "Hs1": "0.31mm", + "Hs2": "8.24mm", + "Bs0": "2.2mm", + "Bs1": "3.37mm", + "Bs2": "4.8mm", + "Rs": "1.5mm", + "SetAngle": "10deg", + } + + def set_winds_params(self): + self.wind_params = { + "Layers": "1", + "ParallelPaths": "1", + "CoilPitch": "9", # coil pitch in slots + "SlotLiner": "0.3mm", + "SpaceLayers": "0.2mm", + "Nc": "68", # turns per coil + } + + def set_mod_params(self): + self.PolePairs = 2 + self.mod_params = { + "Poles": f"2*{self.PolePairs}", + "ModelLength": "85mm", + "SymmetryFactor": "Poles", + "StatorSkewAngle": "0deg", + } + + def set_rot_points(self): + self.rot_points = [ + ["DiaShaft/2*cos(360deg/SymmetryFactor)", "DiaShaft/2*sin(360deg/SymmetryFactor)", "0mm"], + ["DiaShaft/2*cos(360deg/(2*SymmetryFactor))", "DiaShaft/2*sin(360deg/(2*SymmetryFactor))", "0mm"], + ["DiaShaft/2", "0mm", "0mm"], + ["DiaStatorGap/2-Airgap", "0mm", "0mm"], + ["(DiaStatorGap/2-Airgap)*cos(360deg/(2*SymmetryFactor))", "(DiaStatorGap/2-Airgap)*sin(360deg/(2*SymmetryFactor))", "0mm"], + ["(DiaStatorGap/2-Airgap)*cos(360deg/SymmetryFactor)", "(DiaStatorGap/2-Airgap)*sin(360deg/SymmetryFactor)", "0mm"], + ] + + def set_derived_params(self): + self.rotor_r_min = self.mm_to_str("geom_params", "DiaShaft") / 2 + self.rotor_r_max = self.mm_to_str("geom_params", "DiaStatorGap") / 2 - self.mm_to_str("geom_params", "Airgap") + + def set_udp_par_list_stator(self): + self.udp_par_list_stator = [ + ["DiaGap", "DiaStatorGap"], + ["DiaYoke", "DiaStatorYoke"], + ["Length", "0mm"], + ["Skew", "0deg"], + ["Slots", "SlotNumber"], + ["SlotType", "SlotType"], + ["Hs0", "Hs0"], + ["Hs01", "0mm"], + ["Hs1", "Hs1"], + ["Hs2", "Hs2"], + ["Bs0", "Bs0"], + ["Bs1", "Bs1"], + ["Bs2", "Bs2"], + ["Rs", "Rs"], + ["FilletType", "0"], + ["HalfSlot", "0"], + ["SegAngle", "0deg"], + ["LenRegion", "0mm"], + ["InfoCore", "0"], + ] + + def build_stator(self, m2d: Maxwell2d) -> None: + self._push_stator_variables(m2d) + shaft_id, region_id, band_id = self._build_vacuum_regions(m2d) + self._assign_rotor_motion(m2d) + stator_id = self._build_stator_core(m2d) + id_coils = self._build_stator_coils(m2d) + self._split_for_symmetry(m2d, [stator_id, shaft_id, region_id, band_id]) + self._assign_boundary_conditions(m2d) + self._assign_stator_mesh(m2d, stator_id, id_coils) + + # core loss + m2d.set_core_losses("Stator", core_loss_on_field=False) + + self.stator_id = stator_id + self.shaft_id = shaft_id + self.region_id = region_id + self.band_id = band_id + self.id_coils = id_coils + + def _assign_rotor_motion(self, m2d: Maxwell2d) -> None: + m2d.assign_rotate_motion( + assignment="Band", + coordinate_system="Global", + axis="Z", + positive_movement=True, + start_position="InitPos", + angular_velocity="RotSpeed", + has_rotation_limits=False, + ) + + def _build_stator_core(self, m2d: Maxwell2d): + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + stator_id = modeler.create_udp( + dll="RMxprt/SlotCore.dll", + parameters=self.udp_par_list_stator, + library="syslib", + name="Stator", + # SolveInside="True", + ) + stator_id.material_name = self.Fe + + return stator_id + + def _build_stator_coils(self, m2d: Maxwell2d): + modeler = m2d.modeler + assert isinstance(modeler, Modeler2D) + + coil_id = modeler.create_rectangle( + origin=["DiaStatorGap/2+Hs0+Hs1+SlotLiner", "-(Bs1/2-SlotLiner)", 0], + sizes=["(Hs2+Rs/2-2*SlotLiner)/Layers-(Layers-1)*SpaceLayers/2", "Bs1-2*SlotLiner", 0], + name="Coil", + material="copper", + ) + modeler.rotate(assignment=coil_id, axis="Z", angle="360deg/SlotNumber/2") + coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", clones="SlotNumber/Poles", create_new_objects=True) + id_coils = modeler.get_objects_w_string(string_name="Coil", case_sensitive=True) + + return id_coils + + def _assign_stator_mesh(self, m2d: Maxwell2d, stator_id, id_coils) -> None: + m2d.mesh.assign_length_mesh( + assignment=id_coils, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="coils", + ) + m2d.mesh.assign_length_mesh( + assignment=stator_id, + inside_selection=True, + maximum_length=3, + maximum_elements=None, + name="stator", + ) + + +class Computation(ComputationBase): + def set_oper_params(self): + f = 50 # [Hz] + RotSpeed = 60 * f / self.geometry.PolePairs # [rpm] + self.oper_params = { + "Im": "1.5*sqrt(2)A", + "epsI": "pi/4", # current angle + "InitPos": "-30deg", + "f": f"{f}Hz", + "RotSpeed": f"{RotSpeed}rpm", + "Nper": "1/6", # number of included periods + "PointPer": "101", # number of time points per period + } + + def set_solution_expressions(self): + self.solution_expressions = "Moving1.Torque" + + def set_output_vars(self): + self.output_vars = { + "pos": "(Moving1.Position -InitPos) * Poles/2", + "cos0": "cos(pos)", + "cos1": "cos(pos-2*PI/3)", + "cos2": "cos(pos-4*PI/3)", + "sin0": "sin(pos)", + "sin1": "sin(pos-2*PI/3)", + "sin2": "sin(pos-4*PI/3)", + "Lad": "L(PhaseA,PhaseA)*cos0 + L(PhaseA,PhaseB)*cos1 + L(PhaseA,PhaseC)*cos2", + "Laq": "L(PhaseA,PhaseA)*sin0 + L(PhaseA,PhaseB)*sin1 + L(PhaseA,PhaseC)*sin2", + "Lbd": "L(PhaseB,PhaseA)*cos0 + L(PhaseB,PhaseB)*cos1 + L(PhaseB,PhaseC)*cos2", + "Lbq": "L(PhaseB,PhaseA)*sin0 + L(PhaseB,PhaseB)*sin1 + L(PhaseB,PhaseC)*sin2", + "Lcd": "L(PhaseC,PhaseA)*cos0 + L(PhaseC,PhaseB)*cos1 + L(PhaseC,PhaseC)*cos2", + "Lcq": "L(PhaseC,PhaseA)*sin0 + L(PhaseC,PhaseB)*sin1 + L(PhaseC,PhaseC)*sin2", + "L_d": "(Lad*cos0 + Lbd*cos1 + Lcd*cos2) * 2/3", + "L_q": "(Laq*sin0 + Lbq*sin1 + Lcq*sin2) * 2/3", + "Flux_d": "(FluxLinkage(PhaseA)*cos0+FluxLinkage(PhaseB)*cos1+FluxLinkage(PhaseC)*cos2)*2/3", + "Flux_q": "-(FluxLinkage(PhaseA)*sin0+FluxLinkage(PhaseB)*sin1+FluxLinkage(PhaseC)*sin2)*2/3", + "Ui_d": "(InducedVoltage(PhaseA)*cos0+InducedVoltage(PhaseB)*cos1+InducedVoltage(PhaseC)*cos2)*2/3", + "Ui_q": "-(InducedVoltage(PhaseA)*sin0+InducedVoltage(PhaseB)*sin1+InducedVoltage(PhaseC)*sin2)*2/3", + "I_d": "(InputCurrent(PhaseA)*cos0 + InputCurrent(PhaseB)*cos1 + InputCurrent(PhaseC)*cos2)*2/3", + "I_q": "-(InputCurrent(PhaseA)*sin0 + InputCurrent(PhaseB)*sin1 + InputCurrent(PhaseC)*sin2)*2/3", + "Irms": "sqrt(I_d^2+I_q^2)/sqrt(2)", + } + + def set_post_params(self): + self.post_params = { # reports + ("InducedVoltage(PhaseA)", "InducedVoltage(PhaseB)", "InducedVoltage(PhaseC)"): "InducedVoltage", + ("Moving1.Torque"): "Torque", + ("InputCurrent(PhaseA)", "InputCurrent(PhaseB)", "InputCurrent(PhaseC)"): "Current", + ( + "FluxLinkage(PhaseA)", + "FluxLinkage(PhaseB)", + "FluxLinkage(PhaseC)", + ): "FluxLinkage", + ("I_d", "I_q"): "Current_dq", + ("Flux_d", "Flux_q"): "FluxLinkage_dq", + ("Ui_d", "Ui_q"): "InducedVoltage_dq", + ("L_d", "L_q"): "Inductance_dq", + } + + def assign_stator_coils(self, m2d: Maxwell2d) -> None: + # Excitations + I_A = "Im * cos(2*pi*f*time+epsI)" + I_B = "Im * cos(2*pi*f*time-120deg+epsI)" + I_C = "Im * cos(2*pi*f*time-240deg+epsI)" + + phases_polarity = ["Positive", "Negative", "Positive"] + phases_name = ["A", "C", "B"] + phases_current = [I_A, I_C, I_B] + i_coil = 0 + for phase_polarity, phase_name, phase_current in zip(phases_polarity, phases_name, phases_current): + names = [] + for _ in range(3): + m2d.assign_coil( + assignment=[self.geometry.id_coils[i_coil]], + conductors_number="Nc", + polarity=phase_polarity, + name=f"CS{i_coil+1}", + ) + names.append(f"CS{i_coil+1}") + i_coil += 1 + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=phase_current, + parallel_branches="ParallelPaths", + name=f"Phase{phase_name}", + ) + m2d.add_winding_coils(assignment=f"Phase{phase_name}", coils=names) + + def inductance_computation(self, m2d: Maxwell2d) -> None: + m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=False) + + def set_variables(self, m2d: Maxwell2d, *args): + pass + + def extract_results(self, solutions): + return solutions.data_magnitude() diff --git a/motors/motor2.py b/motors/motor2.py new file mode 100644 index 0000000..e3d69b8 --- /dev/null +++ b/motors/motor2.py @@ -0,0 +1,346 @@ +import numpy as np +from ansys.aedt.core import Maxwell2d + +from machine_design.design_computation import ComputationBase + +from .motor1 import Geometry + + +class Geometry2(Geometry): + def set_geom_params(self): + super().set_geom_params() + self.geom_params["SlotNumber"] = "40" + + def set_slot_params(self): + super().set_slot_params() + self.slot_params["Bs1"] = "3.0mm" + self.slot_params["Bs2"] = "4.3mm" + self.slot_params["SetAngle"] = "9deg" + + def set_winds_params(self): + super().set_winds_params() + self.wind_params["Nc"] = "113" + + def set_derived_params(self): + pass + + +class Computation2(ComputationBase): + def set_oper_params(self): + f = 50 # [Hz] + RotSpeed = 60 * f / self.geometry.PolePairs # [rpm] + self.oper_params = { + "Id1": "0.0A", + "Iq1": "0.0A", + "Id3": "0.0A", + "Iq3": "0.0A", + "epsI1": "atan2(Iq1,Id1)", # current angle, 1st harmonic + "epsI3": "atan2(Iq3,Id3)", # current angle, 1st harmonic + "Im1": "sqrt(Id1^2+Iq1^2)", + "Im3": "sqrt(Id3^2+Iq3^2)", + "InitPos": "-45deg", + "f": f"{f}Hz", + "RotSpeed": f"{RotSpeed}rpm", + "Nper": "1/10", # number of included periods + "PointPer": "101", # number of time points per period + } + + def set_solution_expressions(self): + self.solution_expressions = [ + "V_d1", + "V_q1", + "V_d3", + "V_q3", + "Flux_e_d1", + "Flux_e_q1", + "Flux_e_d3", + "Flux_e_q3", + "I_d1", + "I_q1", + "I_d3", + "I_q3", + "Ld1", + "Ld1q1", + "Ld1d3", + "Ld1q3", + "Lq1", + "Lq1d3", + "Lq1q3", + "Ld3", + "Ld3q3", + "Lq3", + "Moving1.Torque", + ] + + def set_output_vars(self): + self.output_vars = { + "PolePairs": "2", + "RotSign": "1", + "Rstat": "19", + "Lew": "0", + "theta_el": "RotSign*(Moving1.Position - InitPos) * PolePairs - pi", + "cos0_1": "cos(1*(theta_el - 2*PI*0/5))", + "sin0_1": "sin(-1*(theta_el - 2*PI*0/5))", + "cos1_1": "cos(1*(theta_el - 2*PI*1/5))", + "sin1_1": "sin(-1*(theta_el - 2*PI*1/5))", + "cos2_1": "cos(1*(theta_el - 2*PI*2/5))", + "sin2_1": "sin(-1*(theta_el - 2*PI*2/5))", + "cos3_1": "cos(1*(theta_el - 2*PI*3/5))", + "sin3_1": "sin(-1*(theta_el - 2*PI*3/5))", + "cos4_1": "cos(1*(theta_el - 2*PI*4/5))", + "sin4_1": "sin(-1*(theta_el - 2*PI*4/5))", + "cos0_3": "cos(3*(theta_el - 2*PI*0/5))", + "sin0_3": "sin(-3*(theta_el - 2*PI*0/5))", + "cos1_3": "cos(3*(theta_el - 2*PI*1/5))", + "sin1_3": "sin(-3*(theta_el - 2*PI*1/5))", + "cos2_3": "cos(3*(theta_el - 2*PI*2/5))", + "sin2_3": "sin(-3*(theta_el - 2*PI*2/5))", + "cos3_3": "cos(3*(theta_el - 2*PI*3/5))", + "sin3_3": "sin(-3*(theta_el - 2*PI*3/5))", + "cos4_3": "cos(3*(theta_el - 2*PI*4/5))", + "sin4_3": "sin(-3*(theta_el - 2*PI*4/5))", + "Flux_d1": "(FluxLinkage(PhaseA)*cos0_1 + FluxLinkage(PhaseB)*cos1_1 + FluxLinkage(PhaseC)*cos2_1 + FluxLinkage(PhaseD)*cos3_1 + FluxLinkage(PhaseE)*cos4_1) * 2/5", + "Flux_q1": "(FluxLinkage(PhaseA)*sin0_1 + FluxLinkage(PhaseB)*sin1_1 + FluxLinkage(PhaseC)*sin2_1 + FluxLinkage(PhaseD)*sin3_1 + FluxLinkage(PhaseE)*sin4_1) * 2/5", + "Flux_d3": "(FluxLinkage(PhaseA)*cos0_3 + FluxLinkage(PhaseB)*cos1_3 + FluxLinkage(PhaseC)*cos2_3 + FluxLinkage(PhaseD)*cos3_3 + FluxLinkage(PhaseE)*cos4_3) * 2/5", + "Flux_q3": "(FluxLinkage(PhaseA)*sin0_3 + FluxLinkage(PhaseB)*sin1_3 + FluxLinkage(PhaseC)*sin2_3 + FluxLinkage(PhaseD)*sin3_3 + FluxLinkage(PhaseE)*sin4_3) * 2/5", + "Vind_d1": "(InducedVoltage(PhaseA)*cos0_1 + InducedVoltage(PhaseB)*cos1_1 + InducedVoltage(PhaseC)*cos2_1 + InducedVoltage(PhaseD)*cos3_1 + InducedVoltage(PhaseE)*cos4_1) * 2/5", + "Vind_q1": "(InducedVoltage(PhaseA)*sin0_1 + InducedVoltage(PhaseB)*sin1_1 + InducedVoltage(PhaseC)*sin2_1 + InducedVoltage(PhaseD)*sin3_1 + InducedVoltage(PhaseE)*sin4_1) * 2/5", + "Vind_d3": "(InducedVoltage(PhaseA)*cos0_3 + InducedVoltage(PhaseB)*cos1_3 + InducedVoltage(PhaseC)*cos2_3 + InducedVoltage(PhaseD)*cos3_3 + InducedVoltage(PhaseE)*cos4_3) * 2/5", + "Vind_q3": "(InducedVoltage(PhaseA)*sin0_3 + InducedVoltage(PhaseB)*sin1_3 + InducedVoltage(PhaseC)*sin2_3 + InducedVoltage(PhaseD)*sin3_3 + InducedVoltage(PhaseE)*sin4_3) * 2/5", + "V_A": "InducedVoltage(PhaseA) + Rstat*InputCurrent(PhaseA) + Lew*deriv(InputCurrent(PhaseA))", + "V_B": "InducedVoltage(PhaseB) + Rstat*InputCurrent(PhaseB) + Lew*deriv(InputCurrent(PhaseB))", + "V_C": "InducedVoltage(PhaseC) + Rstat*InputCurrent(PhaseC) + Lew*deriv(InputCurrent(PhaseC))", + "V_D": "InducedVoltage(PhaseD) + Rstat*InputCurrent(PhaseD) + Lew*deriv(InputCurrent(PhaseD))", + "V_E": "InducedVoltage(PhaseE) + Rstat*InputCurrent(PhaseE) + Lew*deriv(InputCurrent(PhaseE))", + "V_AC": "V_A - V_C", + "V_BD": "V_B - V_D", + "V_CE": "V_C - V_E", + "V_DA": "V_D - V_A", + "V_EB": "V_E - V_B", + "Vterm_A": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", + "Vterm_B": "1/5*(2*V_AC + 4*V_BD + 1*V_CE + 3*V_DA)", + "Vterm_C": "1/5*(-3*V_AC + -1*V_BD + 1*V_CE + -2*V_DA)", + "Vterm_D": "1/5*(2*V_AC + -1*V_BD + 1*V_CE + 3*V_DA)", + "Vterm_E": "1/5*(-3*V_AC + -1*V_BD + -4*V_CE + -2*V_DA)", + "I_d1": "(InputCurrent(PhaseA)*cos0_1 + InputCurrent(PhaseB)*cos1_1 + InputCurrent(PhaseC)*cos2_1 + InputCurrent(PhaseD)*cos3_1 + InputCurrent(PhaseE)*cos4_1) * 2/5", + "I_q1": "(InputCurrent(PhaseA)*sin0_1 + InputCurrent(PhaseB)*sin1_1 + InputCurrent(PhaseC)*sin2_1 + InputCurrent(PhaseD)*sin3_1 + InputCurrent(PhaseE)*sin4_1) * 2/5", + "I_d3": "(InputCurrent(PhaseA)*cos0_3 + InputCurrent(PhaseB)*cos1_3 + InputCurrent(PhaseC)*cos2_3 + InputCurrent(PhaseD)*cos3_3 + InputCurrent(PhaseE)*cos4_3) * 2/5", + "I_q3": "(InputCurrent(PhaseA)*sin0_3 + InputCurrent(PhaseB)*sin1_3 + InputCurrent(PhaseC)*sin2_3 + InputCurrent(PhaseD)*sin3_3 + InputCurrent(PhaseE)*sin4_3) * 2/5", + "V_d1": "(V_A*cos0_1 + V_B*cos1_1 + V_C*cos2_1 + V_D*cos3_1 + V_E*cos4_1) * 2/5", + "V_q1": "(V_A*sin0_1 + V_B*sin1_1 + V_C*sin2_1 + V_D*sin3_1 + V_E*sin4_1) * 2/5", + "V_d3": "(V_A*cos0_3 + V_B*cos1_3 + V_C*cos2_3 + V_D*cos3_3 + V_E*cos4_3) * 2/5", + "V_q3": "(V_A*sin0_3 + V_B*sin1_3 + V_C*sin2_3 + V_D*sin3_3 + V_E*sin4_3) * 2/5", + "L0d_1": "L(PhaseA,PhaseA)*cos0_1 + L(PhaseA,PhaseB)*cos1_1 + L(PhaseA,PhaseC)*cos2_1 + L(PhaseA,PhaseD)*cos3_1 + L(PhaseA,PhaseE)*cos4_1", + "L0q_1": "L(PhaseA,PhaseA)*sin0_1 + L(PhaseA,PhaseB)*sin1_1 + L(PhaseA,PhaseC)*sin2_1 + L(PhaseA,PhaseD)*sin3_1 + L(PhaseA,PhaseE)*sin4_1", + "L1d_1": "L(PhaseB,PhaseA)*cos0_1 + L(PhaseB,PhaseB)*cos1_1 + L(PhaseB,PhaseC)*cos2_1 + L(PhaseB,PhaseD)*cos3_1 + L(PhaseB,PhaseE)*cos4_1", + "L1q_1": "L(PhaseB,PhaseA)*sin0_1 + L(PhaseB,PhaseB)*sin1_1 + L(PhaseB,PhaseC)*sin2_1 + L(PhaseB,PhaseD)*sin3_1 + L(PhaseB,PhaseE)*sin4_1", + "L2d_1": "L(PhaseC,PhaseA)*cos0_1 + L(PhaseC,PhaseB)*cos1_1 + L(PhaseC,PhaseC)*cos2_1 + L(PhaseC,PhaseD)*cos3_1 + L(PhaseC,PhaseE)*cos4_1", + "L2q_1": "L(PhaseC,PhaseA)*sin0_1 + L(PhaseC,PhaseB)*sin1_1 + L(PhaseC,PhaseC)*sin2_1 + L(PhaseC,PhaseD)*sin3_1 + L(PhaseC,PhaseE)*sin4_1", + "L3d_1": "L(PhaseD,PhaseA)*cos0_1 + L(PhaseD,PhaseB)*cos1_1 + L(PhaseD,PhaseC)*cos2_1 + L(PhaseD,PhaseD)*cos3_1 + L(PhaseD,PhaseE)*cos4_1", + "L3q_1": "L(PhaseD,PhaseA)*sin0_1 + L(PhaseD,PhaseB)*sin1_1 + L(PhaseD,PhaseC)*sin2_1 + L(PhaseD,PhaseD)*sin3_1 + L(PhaseD,PhaseE)*sin4_1", + "L4d_1": "L(PhaseE,PhaseA)*cos0_1 + L(PhaseE,PhaseB)*cos1_1 + L(PhaseE,PhaseC)*cos2_1 + L(PhaseE,PhaseD)*cos3_1 + L(PhaseE,PhaseE)*cos4_1", + "L4q_1": "L(PhaseE,PhaseA)*sin0_1 + L(PhaseE,PhaseB)*sin1_1 + L(PhaseE,PhaseC)*sin2_1 + L(PhaseE,PhaseD)*sin3_1 + L(PhaseE,PhaseE)*sin4_1", + "L0d_3": "L(PhaseA,PhaseA)*cos0_3 + L(PhaseA,PhaseB)*cos1_3 + L(PhaseA,PhaseC)*cos2_3 + L(PhaseA,PhaseD)*cos3_3 + L(PhaseA,PhaseE)*cos4_3", + "L0q_3": "L(PhaseA,PhaseA)*sin0_3 + L(PhaseA,PhaseB)*sin1_3 + L(PhaseA,PhaseC)*sin2_3 + L(PhaseA,PhaseD)*sin3_3 + L(PhaseA,PhaseE)*sin4_3", + "L1d_3": "L(PhaseB,PhaseA)*cos0_3 + L(PhaseB,PhaseB)*cos1_3 + L(PhaseB,PhaseC)*cos2_3 + L(PhaseB,PhaseD)*cos3_3 + L(PhaseB,PhaseE)*cos4_3", + "L1q_3": "L(PhaseB,PhaseA)*sin0_3 + L(PhaseB,PhaseB)*sin1_3 + L(PhaseB,PhaseC)*sin2_3 + L(PhaseB,PhaseD)*sin3_3 + L(PhaseB,PhaseE)*sin4_3", + "L2d_3": "L(PhaseC,PhaseA)*cos0_3 + L(PhaseC,PhaseB)*cos1_3 + L(PhaseC,PhaseC)*cos2_3 + L(PhaseC,PhaseD)*cos3_3 + L(PhaseC,PhaseE)*cos4_3", + "L2q_3": "L(PhaseC,PhaseA)*sin0_3 + L(PhaseC,PhaseB)*sin1_3 + L(PhaseC,PhaseC)*sin2_3 + L(PhaseC,PhaseD)*sin3_3 + L(PhaseC,PhaseE)*sin4_3", + "L3d_3": "L(PhaseD,PhaseA)*cos0_3 + L(PhaseD,PhaseB)*cos1_3 + L(PhaseD,PhaseC)*cos2_3 + L(PhaseD,PhaseD)*cos3_3 + L(PhaseD,PhaseE)*cos4_3", + "L3q_3": "L(PhaseD,PhaseA)*sin0_3 + L(PhaseD,PhaseB)*sin1_3 + L(PhaseD,PhaseC)*sin2_3 + L(PhaseD,PhaseD)*sin3_3 + L(PhaseD,PhaseE)*sin4_3", + "L4d_3": "L(PhaseE,PhaseA)*cos0_3 + L(PhaseE,PhaseB)*cos1_3 + L(PhaseE,PhaseC)*cos2_3 + L(PhaseE,PhaseD)*cos3_3 + L(PhaseE,PhaseE)*cos4_3", + "L4q_3": "L(PhaseE,PhaseA)*sin0_3 + L(PhaseE,PhaseB)*sin1_3 + L(PhaseE,PhaseC)*sin2_3 + L(PhaseE,PhaseD)*sin3_3 + L(PhaseE,PhaseE)*sin4_3", + "Ld1": "(L0d_1*cos0_1 + L1d_1*cos1_1 + L2d_1*cos2_1 + L3d_1*cos3_1 + L4d_1*cos4_1) * 2/5", + "Ld1q1": "(L0d_1*sin0_1 + L1d_1*sin1_1 + L2d_1*sin2_1 + L3d_1*sin3_1 + L4d_1*sin4_1) * 2/5", + "Lq1d1": "(L0q_1*cos0_1 + L1q_1*cos1_1 + L2q_1*cos2_1 + L3q_1*cos3_1 + L4q_1*cos4_1) * 2/5", + "Lq1": "(L0q_1*sin0_1 + L1q_1*sin1_1 + L2q_1*sin2_1 + L3q_1*sin3_1 + L4q_1*sin4_1) * 2/5", + "Ld1d3": "(L0d_1*cos0_3 + L1d_1*cos1_3 + L2d_1*cos2_3 + L3d_1*cos3_3 + L4d_1*cos4_3) * 2/5", + "Ld1q3": "(L0d_1*sin0_3 + L1d_1*sin1_3 + L2d_1*sin2_3 + L3d_1*sin3_3 + L4d_1*sin4_3) * 2/5", + "Lq1d3": "(L0q_1*cos0_3 + L1q_1*cos1_3 + L2q_1*cos2_3 + L3q_1*cos3_3 + L4q_1*cos4_3) * 2/5", + "Lq1q3": "(L0q_1*sin0_3 + L1q_1*sin1_3 + L2q_1*sin2_3 + L3q_1*sin3_3 + L4q_1*sin4_3) * 2/5", + "Ld3d1": "(L0d_3*cos0_1 + L1d_3*cos1_1 + L2d_3*cos2_1 + L3d_3*cos3_1 + L4d_3*cos4_1) * 2/5", + "Ld3q1": "(L0d_3*sin0_1 + L1d_3*sin1_1 + L2d_3*sin2_1 + L3d_3*sin3_1 + L4d_3*sin4_1) * 2/5", + "Lq3d1": "(L0q_3*cos0_1 + L1q_3*cos1_1 + L2q_3*cos2_1 + L3q_3*cos3_1 + L4q_3*cos4_1) * 2/5", + "Lq3q1": "(L0q_3*sin0_1 + L1q_3*sin1_1 + L2q_3*sin2_1 + L3q_3*sin3_1 + L4q_3*sin4_1) * 2/5", + "Ld3": "(L0d_3*cos0_3 + L1d_3*cos1_3 + L2d_3*cos2_3 + L3d_3*cos3_3 + L4d_3*cos4_3) * 2/5", + "Ld3q3": "(L0d_3*sin0_3 + L1d_3*sin1_3 + L2d_3*sin2_3 + L3d_3*sin3_3 + L4d_3*sin4_3) * 2/5", + "Lq3d3": "(L0q_3*cos0_3 + L1q_3*cos1_3 + L2q_3*cos2_3 + L3q_3*cos3_3 + L4q_3*cos4_3) * 2/5", + "Lq3": "(L0q_3*sin0_3 + L1q_3*sin1_3 + L2q_3*sin2_3 + L3q_3*sin3_3 + L4q_3*sin4_3) * 2/5", + "Flux_e_d1": "Flux_d1 - (Ld1*I_d1 + Ld1q1*I_q1 + Ld1d3*I_d3 + Ld1q3*I_q3)", + "Flux_e_q1": "Flux_q1 - (Lq1d1*I_d1 + Lq1*I_q1 + Lq1d3*I_d3 + Lq1q3*I_q3)", + "Flux_e_d3": "Flux_d3 - (Ld3d1*I_d1 + Ld3q1*I_q1 + Ld3*I_d3 + Ld3q3*I_q3)", + "Flux_e_q3": "Flux_q3 - (Lq3d1*I_d1 + Lq3q1*I_q1 + Lq3d3*I_d3 + Lq3*I_q3)", + "Torque_dq": "5/2*PolePairs*(1*(Flux_d1*I_q1 - Flux_q1*I_d1) + 3*(Flux_d3*I_q3 - Flux_q3*I_d3))", + } + + def set_post_params(self): + self.post_params = { # reports + ( + "InducedVoltage(PhaseA)", + "InducedVoltage(PhaseB)", + "InducedVoltage(PhaseC)", + "InducedVoltage(PhaseD)", + "InducedVoltage(PhaseE)", + ): "InducedVoltage", + ("Moving1.Torque"): "Torque", + ( + "InputCurrent(PhaseA)", + "InputCurrent(PhaseB)", + "InputCurrent(PhaseC)", + "InputCurrent(PhaseD)", + "InputCurrent(PhaseE)", + ): "Current", + ( + "FluxLinkage(PhaseA)", + "FluxLinkage(PhaseB)", + "FluxLinkage(PhaseC)", + "FluxLinkage(PhaseD)", + "FluxLinkage(PhaseE)", + ): "FluxLinkage", + ("I_d1", "I_q1", "I_d3", "I_q3"): "Current_dq", + ("Flux_d1", "Flux_q1", "Flux_d3", "Flux_q3"): "FluxLinkage_dq", + ("Flux_e_d1", "Flux_e_q1", "Flux_e_d3", "Flux_e_q3"): "FluxLinkage excitation_dq", + ("Vind_d1", "Vind_q1", "Vind_d3", "Vind_q3"): "InducedVoltage_dq", + ("V_d1", "V_q1", "V_d3", "V_q3"): "TerminalVoltage_dq", + ("Ld1", "Lq1", "Ld3", "Lq3"): "Inductance_dq main", + ("Ld1q1", "Ld1d3", "Ld1q3", "Lq1d3", "Lq1q3", "Ld3q3"): "Inductance_dq cross-coupling", + } + + def assign_stator_coils(self, m2d: Maxwell2d) -> None: + # Excitations + I_A = "Im1*cos(2*pi*f*time+epsI1-pi) + Im3*cos(3*(2*pi*f*time)+epsI3-pi)" + I_B = "Im1*cos(2*pi*f*time-72deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-72deg)+epsI3-pi)" + I_C = "Im1*cos(2*pi*f*time-144deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-144deg)+epsI3-pi)" + I_D = "Im1*cos(2*pi*f*time-216deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-216deg)+epsI3-pi)" + I_E = "Im1*cos(2*pi*f*time-288deg+epsI1-pi) + Im3*cos(3*(2*pi*f*time-288deg)+epsI3-pi)" + m2d.assign_coil + # Define phase windings + m2d.assign_coil( + assignment=["Coil"], + conductors_number="Nc", + polarity="Positive", + name="CS1", + ) + m2d.assign_coil( + assignment=["Coil_1"], + conductors_number="Nc", + polarity="Negative", + name="CS2", + ) + m2d.assign_coil( + assignment=["Coil_2"], + conductors_number="Nc", + polarity="Negative", + name="CS3", + ) + m2d.assign_coil( + assignment=["Coil_3"], + conductors_number="Nc", + polarity="Positive", + name="CS4", + ) + m2d.assign_coil( + assignment=["Coil_4"], + conductors_number="Nc", + polarity="Positive", + name="CS5", + ) + m2d.assign_coil( + assignment=["Coil_5"], + conductors_number="Nc", + polarity="Negative", + name="CS6", + ) + m2d.assign_coil( + assignment=["Coil_6"], + conductors_number="Nc", + polarity="Negative", + name="CS7", + ) + m2d.assign_coil( + assignment=["Coil_7"], + conductors_number="Nc", + polarity="Positive", + name="CS8", + ) + m2d.assign_coil( + assignment=["Coil_8"], + conductors_number="Nc", + polarity="Positive", + name="CS9", + ) + m2d.assign_coil( + assignment=["Coil_9"], + conductors_number="Nc", + polarity="Negative", + name="CS10", + ) + + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_A, + parallel_branches="ParallelPaths", + name="PhaseA", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_B, + parallel_branches="ParallelPaths", + name="PhaseB", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_C, + parallel_branches="ParallelPaths", + name="PhaseC", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_D, + parallel_branches="ParallelPaths", + name="PhaseD", + ) + m2d.assign_winding( + assignment=None, + winding_type="Current", + is_solid=False, + current=I_E, + parallel_branches="ParallelPaths", + name="PhaseE", + ) + + m2d.add_winding_coils(assignment="PhaseA", coils=["CS1", "CS10"]) + m2d.add_winding_coils(assignment="PhaseB", coils=["CS4", "CS5"]) + m2d.add_winding_coils(assignment="PhaseC", coils=["CS8", "CS9"]) + m2d.add_winding_coils(assignment="PhaseD", coils=["CS2", "CS3"]) + m2d.add_winding_coils(assignment="PhaseE", coils=["CS6", "CS7"]) + + def inductance_computation(self, m2d: Maxwell2d) -> None: + m2d.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=True) + + def set_variables(self, m2d: Maxwell2d, Id1, Iq1, Id3, Iq3): + m2d.variable_manager["Id1"] = f"{Id1}A" + m2d.variable_manager["Iq1"] = f"{Iq1}A" + m2d.variable_manager["Id3"] = f"{Id3}A" + m2d.variable_manager["Iq3"] = f"{Iq3}A" + + def extract_results(self, solutions): + # TODO: this works only because torque is the last one in the array + out = np.zeros(len(self.solution_expressions)) + for i, expr in enumerate(self.solution_expressions): + data = solutions.data_real(expr) + val = float(np.mean(data[:-1])) + + if expr.startswith("L_"): + val /= 1e9 + + out[i] = val + return data diff --git a/pyproject.toml b/pyproject.toml index ebdc752..442731f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dev = [ [tool.setuptools] packages = [ "machine_design", + "motors", ] [tool.pytest.ini_options] diff --git a/tests/test_ansys_regression.py b/tests/test_ansys_regression.py index 9a9719f..d03ccdf 100644 --- a/tests/test_ansys_regression.py +++ b/tests/test_ansys_regression.py @@ -16,9 +16,8 @@ import pytest from machine_design.design import Design as LiveDesign -from machine_design.design_computation import Computation -from machine_design.design_geometry import Geometry from machine_design.generators import HacklGenerator_OneLambda +from motors.motor1 import Computation, Geometry pytestmark = pytest.mark.ansys diff --git a/tests/test_design.py b/tests/test_design.py index c1e27e9..bdeae90 100644 --- a/tests/test_design.py +++ b/tests/test_design.py @@ -2,8 +2,7 @@ import pytest -from machine_design.design_computation import Computation -from machine_design.design_geometry import Geometry +from motors.motor1 import Computation, Geometry @pytest.fixture diff --git a/tests/test_design2.py b/tests/test_design2.py index 490211d..56d6cff 100644 --- a/tests/test_design2.py +++ b/tests/test_design2.py @@ -1,7 +1,7 @@ import pytest -from machine_design.design_computation import Computation2 -from machine_design.design_geometry import Geometry, Geometry2 +from motors.motor1 import Geometry +from motors.motor2 import Computation2, Geometry2 @pytest.fixture From dfc6048fd16e871bd24b370fcda172eac02847e2 Mon Sep 17 00:00:00 2001 From: sadda Date: Tue, 14 Jul 2026 08:57:52 +0000 Subject: [PATCH 19/29] Refactor --- machine_design/load.py | 8 ++++++++ notebooks/run.py | 5 ++++- notebooks/run_optimization.py | 5 ++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/machine_design/load.py b/machine_design/load.py index cd12b03..3eec054 100644 --- a/machine_design/load.py +++ b/machine_design/load.py @@ -1,6 +1,8 @@ import os from .design import Design +from .design_computation import ComputationBase +from .design_geometry import GeometryBase def load_design( @@ -8,6 +10,8 @@ def load_design( project_name, design_name, aedt_version, + geometry: GeometryBase, + computation: ComputationBase, new_desktop=False, non_graphical=True, close_on_exit=True, @@ -19,6 +23,8 @@ def load_design( project_name, design_name, file_name_aedt, + geometry, + computation, version=aedt_version, non_graphical=non_graphical, new_desktop=new_desktop, @@ -28,6 +34,8 @@ def load_design( else: return Design.load( file_name_aedt, + geometry, + computation, version=aedt_version, non_graphical=non_graphical, new_desktop=new_desktop, diff --git a/notebooks/run.py b/notebooks/run.py index 1686e05..1d7f84d 100644 --- a/notebooks/run.py +++ b/notebooks/run.py @@ -14,6 +14,7 @@ plot_barriers, save_params, ) +from motors.motor1 import Computation, Geometry aedt_version = "2024.1" n_designs = 50 @@ -30,7 +31,9 @@ os.makedirs(path, exist_ok=True) file_name_aedt = f"{path_data}/{project_name}.aedt" -design = load_design(file_name_aedt, project_name, design_name, aedt_version) +geometry = Geometry() +computation = Computation(geometry) +design = load_design(file_name_aedt, project_name, design_name, aedt_version, geometry, computation) generators = [ HacklGenerator_OneLambda(design, r_stator_end, offset=offset), HacklGenerator_SixLambdas(design, r_stator_end, offset=offset), diff --git a/notebooks/run_optimization.py b/notebooks/run_optimization.py index 1e61d1a..bb5a2b5 100644 --- a/notebooks/run_optimization.py +++ b/notebooks/run_optimization.py @@ -22,6 +22,7 @@ objective, objective_transform, ) +from motors.motor1 import Computation, Geometry torch.set_default_dtype(torch.float64) @@ -44,7 +45,9 @@ os.makedirs(path_data, exist_ok=True) file_name_aedt = f"{path_data}/{project_name}.aedt" -design = load_design(file_name_aedt, project_name, design_name, aedt_version) +geometry = Geometry() +computation = Computation(geometry) +design = load_design(file_name_aedt, project_name, design_name, aedt_version, geometry, computation) generators = [ HacklGenerator_OneLambda(design, r_stator_end, offset=offset), HacklGenerator_SixLambdas(design, r_stator_end, offset=offset), From 8ef800f0c077b39bfd7ad25e91c5fadb37bdf58c Mon Sep 17 00:00:00 2001 From: sadda Date: Tue, 14 Jul 2026 09:25:13 +0000 Subject: [PATCH 20/29] Refactor --- machine_design/{design_computation.py => designs/computation.py} | 0 machine_design/{ => designs}/design.py | 0 machine_design/{design_geometry.py => designs/geometry.py} | 0 machine_design/{ => designs}/load.py | 0 machine_design/{ => optimization}/generators.py | 0 machine_design/{ => optimization}/geometry.py | 0 machine_design/{ => optimization}/optimization.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename machine_design/{design_computation.py => designs/computation.py} (100%) rename machine_design/{ => designs}/design.py (100%) rename machine_design/{design_geometry.py => designs/geometry.py} (100%) rename machine_design/{ => designs}/load.py (100%) rename machine_design/{ => optimization}/generators.py (100%) rename machine_design/{ => optimization}/geometry.py (100%) rename machine_design/{ => optimization}/optimization.py (100%) diff --git a/machine_design/design_computation.py b/machine_design/designs/computation.py similarity index 100% rename from machine_design/design_computation.py rename to machine_design/designs/computation.py diff --git a/machine_design/design.py b/machine_design/designs/design.py similarity index 100% rename from machine_design/design.py rename to machine_design/designs/design.py diff --git a/machine_design/design_geometry.py b/machine_design/designs/geometry.py similarity index 100% rename from machine_design/design_geometry.py rename to machine_design/designs/geometry.py diff --git a/machine_design/load.py b/machine_design/designs/load.py similarity index 100% rename from machine_design/load.py rename to machine_design/designs/load.py diff --git a/machine_design/generators.py b/machine_design/optimization/generators.py similarity index 100% rename from machine_design/generators.py rename to machine_design/optimization/generators.py diff --git a/machine_design/geometry.py b/machine_design/optimization/geometry.py similarity index 100% rename from machine_design/geometry.py rename to machine_design/optimization/geometry.py diff --git a/machine_design/optimization.py b/machine_design/optimization/optimization.py similarity index 100% rename from machine_design/optimization.py rename to machine_design/optimization/optimization.py From 650ddf939e5648306e9e4b67a59e07c4e9882c4f Mon Sep 17 00:00:00 2001 From: sadda Date: Tue, 14 Jul 2026 09:28:48 +0000 Subject: [PATCH 21/29] Refactor --- machine_design/__init__.py | 12 ------------ machine_design/designs/__init__.py | 2 ++ machine_design/designs/computation.py | 2 +- machine_design/designs/design.py | 4 ++-- machine_design/designs/load.py | 4 ++-- machine_design/optimization/__init__.py | 10 ++++++++++ machine_design/optimization/generators.py | 2 +- motors/motor1.py | 4 ++-- motors/motor2.py | 2 +- notebooks/run.py | 4 ++-- notebooks/run_optimization.py | 4 ++-- pyproject.toml | 2 ++ tests/test_ansys_regression.py | 4 ++-- 13 files changed, 29 insertions(+), 27 deletions(-) create mode 100644 machine_design/designs/__init__.py create mode 100644 machine_design/optimization/__init__.py diff --git a/machine_design/__init__.py b/machine_design/__init__.py index f768f76..e69de29 100644 --- a/machine_design/__init__.py +++ b/machine_design/__init__.py @@ -1,12 +0,0 @@ -from .design import Design -from .generators import ( - FourStupid, - HacklGenerator_3BrokenLines, - HacklGenerator_OneLambda, - HacklGenerator_SixLambdas, - HacklGenerator_TwoLambdas, - save_params, -) -from .geometry import analyze_results, plot_barriers, rotate -from .load import load_design -from .optimization import init_points, objective, objective_single, objective_transform diff --git a/machine_design/designs/__init__.py b/machine_design/designs/__init__.py new file mode 100644 index 0000000..3686109 --- /dev/null +++ b/machine_design/designs/__init__.py @@ -0,0 +1,2 @@ +from .design import Design +from .load import load_design diff --git a/machine_design/designs/computation.py b/machine_design/designs/computation.py index d2248fc..f75c095 100644 --- a/machine_design/designs/computation.py +++ b/machine_design/designs/computation.py @@ -2,7 +2,7 @@ from ansys.aedt.core import Maxwell2d -from .design_geometry import GeometryBase +from .geometry import GeometryBase class ComputationBase(ABC): diff --git a/machine_design/designs/design.py b/machine_design/designs/design.py index 83f53e7..3ec1bff 100644 --- a/machine_design/designs/design.py +++ b/machine_design/designs/design.py @@ -1,7 +1,7 @@ from ansys.aedt.core import Desktop, Maxwell2d -from .design_computation import ComputationBase -from .design_geometry import GeometryBase +from .computation import ComputationBase +from .geometry import GeometryBase class Design: diff --git a/machine_design/designs/load.py b/machine_design/designs/load.py index 3eec054..a6d01a0 100644 --- a/machine_design/designs/load.py +++ b/machine_design/designs/load.py @@ -1,8 +1,8 @@ import os +from .computation import ComputationBase from .design import Design -from .design_computation import ComputationBase -from .design_geometry import GeometryBase +from .geometry import GeometryBase def load_design( diff --git a/machine_design/optimization/__init__.py b/machine_design/optimization/__init__.py new file mode 100644 index 0000000..200594f --- /dev/null +++ b/machine_design/optimization/__init__.py @@ -0,0 +1,10 @@ +from .generators import ( + FourStupid, + HacklGenerator_3BrokenLines, + HacklGenerator_OneLambda, + HacklGenerator_SixLambdas, + HacklGenerator_TwoLambdas, + save_params, +) +from .geometry import analyze_results, plot_barriers, rotate +from .optimization import init_points, objective, objective_single, objective_transform diff --git a/machine_design/optimization/generators.py b/machine_design/optimization/generators.py index 7ffa39f..52612c0 100644 --- a/machine_design/optimization/generators.py +++ b/machine_design/optimization/generators.py @@ -6,7 +6,7 @@ from scipy.interpolate import CubicSpline from shapely.geometry import LineString -from .design import Design +from ..designs.design import Design from .geometry import rotate diff --git a/motors/motor1.py b/motors/motor1.py index 7466c4b..63713fb 100644 --- a/motors/motor1.py +++ b/motors/motor1.py @@ -1,8 +1,8 @@ from ansys.aedt.core import Maxwell2d from ansys.aedt.core.modeler.modeler_2d import Modeler2D -from machine_design.design_computation import ComputationBase -from machine_design.design_geometry import GeometryBase +from machine_design.designs.computation import ComputationBase +from machine_design.designs.geometry import GeometryBase class Geometry(GeometryBase): diff --git a/motors/motor2.py b/motors/motor2.py index e3d69b8..7c7d414 100644 --- a/motors/motor2.py +++ b/motors/motor2.py @@ -1,7 +1,7 @@ import numpy as np from ansys.aedt.core import Maxwell2d -from machine_design.design_computation import ComputationBase +from machine_design.designs.computation import ComputationBase from .motor1 import Geometry diff --git a/notebooks/run.py b/notebooks/run.py index 1d7f84d..34728fe 100644 --- a/notebooks/run.py +++ b/notebooks/run.py @@ -5,12 +5,12 @@ import numpy as np import pandas as pd -from machine_design import ( +from machine_design.designs import load_design +from machine_design.optimization import ( HacklGenerator_3BrokenLines, HacklGenerator_OneLambda, HacklGenerator_SixLambdas, analyze_results, - load_design, plot_barriers, save_params, ) diff --git a/notebooks/run_optimization.py b/notebooks/run_optimization.py index bb5a2b5..b32a971 100644 --- a/notebooks/run_optimization.py +++ b/notebooks/run_optimization.py @@ -13,12 +13,12 @@ from botorch.utils.transforms import normalize, unnormalize from gpytorch.mlls import ExactMarginalLogLikelihood -from machine_design import ( +from machine_design.designs import load_design +from machine_design.optimization import ( HacklGenerator_3BrokenLines, HacklGenerator_OneLambda, HacklGenerator_SixLambdas, init_points, - load_design, objective, objective_transform, ) diff --git a/pyproject.toml b/pyproject.toml index 442731f..ab82979 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,8 @@ dev = [ [tool.setuptools] packages = [ "machine_design", + "machine_design.designs", + "machine_design.optimization", "motors", ] diff --git a/tests/test_ansys_regression.py b/tests/test_ansys_regression.py index d03ccdf..971fad1 100644 --- a/tests/test_ansys_regression.py +++ b/tests/test_ansys_regression.py @@ -15,8 +15,8 @@ import numpy as np import pytest -from machine_design.design import Design as LiveDesign -from machine_design.generators import HacklGenerator_OneLambda +from machine_design.designs.design import Design as LiveDesign +from machine_design.optimization.generators import HacklGenerator_OneLambda from motors.motor1 import Computation, Geometry pytestmark = pytest.mark.ansys From b5c444e993ed0b7f7a59cc3c883f1b2c3c33d61c Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Tue, 14 Jul 2026 15:32:54 +0200 Subject: [PATCH 22/29] fix: extract_results returns out instead of data --- motors/motor2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motors/motor2.py b/motors/motor2.py index 7c7d414..0162493 100644 --- a/motors/motor2.py +++ b/motors/motor2.py @@ -343,4 +343,4 @@ def extract_results(self, solutions): val /= 1e9 out[i] = val - return data + return out From f3925437c26f2112bbc566120e0c024537922448 Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Tue, 14 Jul 2026 15:57:11 +0200 Subject: [PATCH 23/29] style: apply ruff formatting --- motors/motor1.py | 4 ++-- tests/test_ansys_regression.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/motors/motor1.py b/motors/motor1.py index 63713fb..ce1c118 100644 --- a/motors/motor1.py +++ b/motors/motor1.py @@ -242,9 +242,9 @@ def assign_stator_coils(self, m2d: Maxwell2d) -> None: assignment=[self.geometry.id_coils[i_coil]], conductors_number="Nc", polarity=phase_polarity, - name=f"CS{i_coil+1}", + name=f"CS{i_coil + 1}", ) - names.append(f"CS{i_coil+1}") + names.append(f"CS{i_coil + 1}") i_coil += 1 m2d.assign_winding( assignment=None, diff --git a/tests/test_ansys_regression.py b/tests/test_ansys_regression.py index 971fad1..2417cbc 100644 --- a/tests/test_ansys_regression.py +++ b/tests/test_ansys_regression.py @@ -78,9 +78,7 @@ def test_legacy_and_live_design_produce_same_torque(tmp_path): legacy_design, live_design = None, None try: - legacy_design, torque_legacy = _build_and_compute( - LegacyDesign, "RegressionTest_legacy", str(tmp_path / "legacy.aedt"), barriers - ) + legacy_design, torque_legacy = _build_and_compute(LegacyDesign, "RegressionTest_legacy", str(tmp_path / "legacy.aedt"), barriers) live_geometry = Geometry() live_computation = Computation(live_geometry) From 13a25cd111e8c1170ef8cd9bf0af39756dca4e9e Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Tue, 14 Jul 2026 16:32:53 +0200 Subject: [PATCH 24/29] ci: add pytest workflow --- .github/workflows/tests.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..929111b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,34 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + pytest: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install package + run: pip install -e ".[dev]" + + - name: Install missing dependencies + run: pip install pandas scipy shapely + + - name: Run tests + run: pytest From ace8daf3bf2edcd583f701a9021103419e944c8f Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Wed, 15 Jul 2026 13:41:50 +0200 Subject: [PATCH 25/29] Move pandas, scipy, shapely into pyproject.toml dependencies Remove the redundant separate install step from the CI workflow now that these are declared as proper dependencies. --- .github/workflows/tests.yml | 3 --- pyproject.toml | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 929111b..744fe69 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,8 +27,5 @@ jobs: - name: Install package run: pip install -e ".[dev]" - - name: Install missing dependencies - run: pip install pandas scipy shapely - - name: Run tests run: pytest diff --git a/pyproject.toml b/pyproject.toml index ab82979..d6e4e24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,9 @@ dependencies=[ 'trame', 'matplotlib', 'botorch', + 'pandas', + 'scipy', + 'shapely', ] [project.optional-dependencies] From ee602eebe0f70be50c5ea7c656a62bcda6d2bdd8 Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Wed, 15 Jul 2026 16:02:54 +0200 Subject: [PATCH 26/29] fix: convert units directly in extract_results --- motors/motor2.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/motors/motor2.py b/motors/motor2.py index 0162493..1f34021 100644 --- a/motors/motor2.py +++ b/motors/motor2.py @@ -333,14 +333,15 @@ def set_variables(self, m2d: Maxwell2d, Id1, Iq1, Id3, Iq3): m2d.variable_manager["Iq3"] = f"{Iq3}A" def extract_results(self, solutions): - # TODO: this works only because torque is the last one in the array - out = np.zeros(len(self.solution_expressions)) - for i, expr in enumerate(self.solution_expressions): + out = {} + for expr in self.solution_expressions: data = solutions.data_real(expr) val = float(np.mean(data[:-1])) - if expr.startswith("L_"): + if expr.startswith("Ld") or expr.startswith("Lq"): val /= 1e9 + elif expr.startswith("I_"): + val /= 1e3 - out[i] = val + out[expr] = val return out From 524dcbcc6f99a7a6d9aa90fc184b3d5f7b2780c5 Mon Sep 17 00:00:00 2001 From: Lukas Adam Date: Wed, 15 Jul 2026 19:39:49 +0200 Subject: [PATCH 27/29] Returning whole array --- motors/motor2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/motors/motor2.py b/motors/motor2.py index 1f34021..27ee297 100644 --- a/motors/motor2.py +++ b/motors/motor2.py @@ -335,13 +335,13 @@ def set_variables(self, m2d: Maxwell2d, Id1, Iq1, Id3, Iq3): def extract_results(self, solutions): out = {} for expr in self.solution_expressions: - data = solutions.data_real(expr) - val = float(np.mean(data[:-1])) + val = solutions.data_real(expr) if expr.startswith("Ld") or expr.startswith("Lq"): val /= 1e9 elif expr.startswith("I_"): val /= 1e3 + # TODO: possibly assign val[:-1]. check whether values are identical out[expr] = val return out From 6ea9a923e1bdf8afaf969dda058070fd44ddef87 Mon Sep 17 00:00:00 2001 From: Lukas Adam Date: Wed, 15 Jul 2026 19:41:37 +0200 Subject: [PATCH 28/29] Ruff --- motors/motor2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/motors/motor2.py b/motors/motor2.py index 27ee297..40c5bd6 100644 --- a/motors/motor2.py +++ b/motors/motor2.py @@ -1,4 +1,3 @@ -import numpy as np from ansys.aedt.core import Maxwell2d from machine_design.designs.computation import ComputationBase From 9a745dac8246c715007de37cbcd6b99e26a8183b Mon Sep 17 00:00:00 2001 From: sadda Date: Wed, 2 Sep 2026 15:31:53 +0000 Subject: [PATCH 29/29] Added tests --- tests/test_ansys_regression.py | 52 +++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/tests/test_ansys_regression.py b/tests/test_ansys_regression.py index 2417cbc..0e87a54 100644 --- a/tests/test_ansys_regression.py +++ b/tests/test_ansys_regression.py @@ -1,8 +1,10 @@ """Golden-master check: tests/legacy/design.py vs. the live machine_design/design.py. -Generates one feasible rotor design with HacklGenerator_OneLambda, builds it with both -the frozen reference implementation and the current implementation, runs both through -Ansys, and asserts the resulting torque waveforms match. Run explicitly with: +Generates two feasible rotor designs (different seeds) with HacklGenerator_OneLambda and +runs each through add_rotor -> compute -> delete_rotor on the SAME Design instance, both +with the frozen reference implementation and the current one - mirroring how an +optimization loop reuses one Design across many candidate rotors. Asserts the resulting +torque waveforms match at each step. Run explicitly with: pytest -m ansys tests/test_ansys_regression.py @@ -24,7 +26,7 @@ AEDT_VERSION = "2024.1" R_STATOR_END = 0.7 OFFSET = 0.35 -SEED = 42 +SEEDS = (42, 43) NUM_CORES = 1 @@ -36,14 +38,14 @@ def _load_legacy_design_class(): return module.Design -def _generate_one_lambda_barriers(): +def _generate_one_lambda_barriers(seed): # rotor_r_min/rotor_r_max are pure-Python derived params, so a throwaway # instance (no Ansys) is enough to size the generator. geometry = Geometry() dummy = LiveDesign(m2d=None, geometry=geometry, computation=Computation(geometry)) generator = HacklGenerator_OneLambda(dummy, R_STATOR_END, offset=OFFSET) - np.random.seed(SEED) + np.random.seed(seed) while True: params = generator.random_parameters() generator.set_parameters(params) @@ -53,8 +55,14 @@ def _generate_one_lambda_barriers(): return barriers -def _build_and_compute(design_cls, project_name, file_name, barriers, extra_args=()): - design = design_cls.create( +def _add_rotor(design, barriers): + design.add_rotor() + for barrier in barriers: + design.add_rotor_barrier(barrier) + + +def _create_design(design_cls, project_name, file_name, extra_args=()): + return design_cls.create( project_name, "Design01", file_name, @@ -64,35 +72,39 @@ def _build_and_compute(design_cls, project_name, file_name, barriers, extra_args new_desktop=False, close_on_exit=False, ) - design.add_rotor() - for barrier in barriers: - design.add_rotor_barrier(barrier) + + +def _compute_torque(design, barriers, add_rotor): + add_rotor(design, barriers) torque = design.compute(NUM_CORES=NUM_CORES) design.delete_rotor() - return design, torque + return torque -def test_legacy_and_live_design_produce_same_torque(tmp_path): - barriers = _generate_one_lambda_barriers() +def test_legacy_and_live_design_match_across_rotor_rebuilds(tmp_path): + barrier_sets = [_generate_one_lambda_barriers(seed) for seed in SEEDS] LegacyDesign = _load_legacy_design_class() legacy_design, live_design = None, None try: - legacy_design, torque_legacy = _build_and_compute(LegacyDesign, "RegressionTest_legacy", str(tmp_path / "legacy.aedt"), barriers) + legacy_design = _create_design(LegacyDesign, "RegressionTest_legacy", str(tmp_path / "legacy.aedt")) live_geometry = Geometry() live_computation = Computation(live_geometry) - live_design, torque_live = _build_and_compute( + live_design = _create_design( LiveDesign, "RegressionTest_live", str(tmp_path / "live.aedt"), - barriers, extra_args=(live_geometry, live_computation), ) - assert torque_legacy is not None - assert torque_live is not None - np.testing.assert_allclose(torque_live, torque_legacy, rtol=1e-6) + for seed, barriers in zip(SEEDS, barrier_sets): + torque_legacy = _compute_torque(legacy_design, barriers, _add_rotor) + torque_live = _compute_torque(live_design, barriers, _add_rotor) + + assert torque_legacy is not None, f"legacy torque is None for seed {seed}" + assert torque_live is not None, f"live torque is None for seed {seed}" + np.testing.assert_allclose(torque_live, torque_legacy, rtol=1e-6, err_msg=f"mismatch for seed {seed}") finally: if live_design is not None: live_design.close_project()