From 09c65066f9d432b44f0c817836d5b91d4b64bfa9 Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Fri, 9 Oct 2026 15:23:34 +0200 Subject: [PATCH 1/3] skipped since output_vars={} --- tests/test_ansys_transforms.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_ansys_transforms.py b/tests/test_ansys_transforms.py index b929707..652ea86 100644 --- a/tests/test_ansys_transforms.py +++ b/tests/test_ansys_transforms.py @@ -15,9 +15,10 @@ both sides of the comparison, so it can only be run while they still exist. Once the Ansys-side voltage variables go, drop this half and keep the flux half. -Marked `ansys`, so it is skipped by default. Run with: +Marked `ansys`, since output_vars={} (noved to python side), this test is always skipped. +Run with: - python -m pytest -m "" tests/test_ansys_transforms.py -v + pytest -m ansys "" tests/test_ansys_transforms.py -v """ import numpy as np @@ -103,7 +104,7 @@ def solved(tmp_path_factory): if design is not None: design.close_project() - +@pytest.mark.skip(reason="output_vars={}") @pytest.mark.parametrize(("label", "phase_exprs", "prefix"), SUBJECTS, ids=[s[0] for s in SUBJECTS]) @pytest.mark.parametrize("harmonic", [1, 3]) def test_python_transform_matches_ansys(solved, label, phase_exprs, prefix, harmonic): From f9b03edc95034a549002e073e42d58f6fc1a200b Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Fri, 9 Oct 2026 15:34:12 +0200 Subject: [PATCH 2/3] ruff format --- tests/test_ansys_transforms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_ansys_transforms.py b/tests/test_ansys_transforms.py index 652ea86..e4abbe1 100644 --- a/tests/test_ansys_transforms.py +++ b/tests/test_ansys_transforms.py @@ -104,6 +104,7 @@ def solved(tmp_path_factory): if design is not None: design.close_project() + @pytest.mark.skip(reason="output_vars={}") @pytest.mark.parametrize(("label", "phase_exprs", "prefix"), SUBJECTS, ids=[s[0] for s in SUBJECTS]) @pytest.mark.parametrize("harmonic", [1, 3]) From 94896acf3d9f3762644a2c300d332c4d0825843f Mon Sep 17 00:00:00 2001 From: haitao-ZCU Date: Fri, 9 Oct 2026 17:02:24 +0200 Subject: [PATCH 3/3] remove test_ansys_transforms, test_synrm_60s_torque_i3_zero, and test_ansys_synrm_60s_solve --- tests/test_ansys_synrm_60s_solve.py | 76 -------------- tests/test_ansys_transforms.py | 131 ------------------------- tests/test_synrm_60s_torque_i3_zero.py | 90 ----------------- 3 files changed, 297 deletions(-) delete mode 100644 tests/test_ansys_synrm_60s_solve.py delete mode 100644 tests/test_ansys_transforms.py delete mode 100644 tests/test_synrm_60s_torque_i3_zero.py diff --git a/tests/test_ansys_synrm_60s_solve.py b/tests/test_ansys_synrm_60s_solve.py deleted file mode 100644 index 7504fa4..0000000 --- a/tests/test_ansys_synrm_60s_solve.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Goal: test both synrm_3f_60s.py and synrm_5f_60s.py can accept the same rotor and solve. - -Run explicitly with: - - pytest -m ansys tests/test_ansys_synrm_60s_solve.py - -Requires a running Ansys Electronics Desktop session and a free license seat. -""" - -import numpy as np -import pytest - -from machine_design.config import load_config -from machine_design.designs.design import Design as LiveDesign -from machine_design.optimization.generators import HacklGenerator_OneLambda -from motors.synrm_3f_60s import Computation as Computation3f -from motors.synrm_3f_60s import Geometry as Geometry3f -from motors.synrm_5f_60s import Computation as Computation5f -from motors.synrm_5f_60s import Geometry as Geometry5f - -pytestmark = pytest.mark.ansys - -AEDT_VERSION = load_config()["aedt_version"] -R_STATOR_END = 0.7 -OFFSET = 0.35 -SEEDS = (42, 43) -NUM_CORES = 1 -Machines = [ - ("synrm_3f_60s", Geometry3f, Computation3f, (1.5, 1.5)), - ("synrm_5f_60s", Geometry5f, Computation5f, (7.0711, 7.0711, 0.0, 0.0)), -] - - -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 = Geometry3f() - dummy = LiveDesign(m2d=None, geometry=geometry, computation=Computation3f(geometry)) - 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 test_synrm_3f_60s_and_5f_60s_solve_with_same_rotor(tmp_path): - for seed in SEEDS: - barriers = _generate_one_lambda_barriers(seed) - - for name, geometry_cls, computation_cls, current_setpoint in Machines: - geometry = geometry_cls() - computation = computation_cls(geometry) - design = LiveDesign.create( - f"Check60_{name}_{seed}", - "Design01", - str(tmp_path / f"{name}_{seed}.aedt"), - geometry, - computation, - version=AEDT_VERSION, - non_graphical=True, - new_desktop=False, - close_on_exit=False, - ) - try: - design.add_rotor() - for barrier in barriers: - design.add_rotor_barrier(barrier) - torque = design.compute(*current_setpoint, NUM_CORES=NUM_CORES) - assert torque is not None, f"{name} seed {seed}: torque is None" - finally: - design.close_project() diff --git a/tests/test_ansys_transforms.py b/tests/test_ansys_transforms.py deleted file mode 100644 index e4abbe1..0000000 --- a/tests/test_ansys_transforms.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Live check that the Python dq transform agrees with the Ansys output variables. - -`machine_design/generic/transforms.py` and `motors/synrm_5f_40s.py`'s `set_output_vars` -implement the same five-phase Park/Clarke transform twice -- once in Python, once as -Ansys expressions.This solves one rotor with both dq1 and dq3 excited, then asserts that -transforming the per-phase quantities in Python reproduces Ansys's own dq outputs. - -Two subjects, deliberately: - -* **flux linkage** -- `FluxLinkage(PhaseA..E)` vs `Flux_d1/q1/d3/q3`. Flux stays in Ansys - (it needs the field solution), so this comparison is permanent: it is the standing guard - on the boundary described in "What runs in Ansys, what runs in Python" (CONTRIBUTING.md). -* **terminal voltage** -- `V_A..V_E` vs `V_d1/q1/d3/q3`. This one is a *one-shot* - equivalence proof. The voltage transforms are slated to move to Python, which deletes - both sides of the comparison, so it can only be run while they still exist. Once the - Ansys-side voltage variables go, drop this half and keep the flux half. - -Marked `ansys`, since output_vars={} (noved to python side), this test is always skipped. -Run with: - - pytest -m ansys "" tests/test_ansys_transforms.py -v -""" - -import numpy as np -import pytest - -from machine_design.config import load_config -from machine_design.designs.design import Design -from machine_design.generic.transforms import to_dq -from machine_design.optimization.generators import HacklGenerator_OneLambda -from motors.synrm_5f_40s import Computation, Geometry - -pytestmark = pytest.mark.ansys - -AEDT_VERSION = load_config()["aedt_version"] -R_STATOR_END = 0.7 -OFFSET = 0.35 -SEED = 42 -NUM_CORES = 1 - -# Both subspaces excited, so the h = 3 comparison is not a check against zero. -# Order is (Id1, Iq1, Id3, Iq3) -- compute() forwards these to set_variables(). -CURRENTS = (0.8, 0.9, 0.2, -0.15) - -PHASES = ["A", "B", "C", "D", "E"] -PHASE_FLUX = [f"FluxLinkage(Phase{p})" for p in PHASES] -PHASE_VOLTAGE = [f"V_{p}" for p in PHASES] - -# (label, per-phase expressions, ansys dq prefix) -SUBJECTS = [("flux", PHASE_FLUX, "Flux"), ("voltage", PHASE_VOLTAGE, "V")] - - -class ComputationWithPhaseQuantities(Computation): - """`Computation`, additionally extracting the per-phase quantities and theta_el. - - The shipped class extracts only the dq quantities, since that is all the optimizers - need. Subclassed rather than edited so the shared anchor is untouched. - """ - - def set_solution_expressions(self): - super().set_solution_expressions() - dq = [f"{prefix}_{axis}{h}" for prefix in ("Flux", "V") for h in (1, 3) for axis in "dq"] - extra = [*PHASE_FLUX, *PHASE_VOLTAGE, "theta_el", *dq] - self.solution_expressions = [*self.solution_expressions, *extra] - - -def _feasible_barriers(geometry): - dummy = Design(m2d=None, geometry=geometry, computation=Computation(geometry)) - generator = HacklGenerator_OneLambda(dummy, R_STATOR_END, offset=OFFSET) - - np.random.seed(SEED) - while True: - generator.set_parameters(generator.random_parameters()) - barriers = generator.split_barriers(generator.generate_barriers()) - if generator.feasible_barriers(barriers): - return barriers - - -@pytest.fixture(scope="module") -def solved(tmp_path_factory): - """One solve, shared by both subjects -- an FEA solve is far too costly to repeat.""" - geometry = Geometry() - computation = ComputationWithPhaseQuantities(geometry) - barriers = _feasible_barriers(geometry) - - design = None - try: - design = Design.create( - "TransformBoundary", - "Design01", - str(tmp_path_factory.mktemp("aedt") / "transform.aedt"), - geometry, - computation, - 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) - return design.compute(*CURRENTS, NUM_CORES=NUM_CORES) - finally: - if design is not None: - design.close_project() - - -@pytest.mark.skip(reason="output_vars={}") -@pytest.mark.parametrize(("label", "phase_exprs", "prefix"), SUBJECTS, ids=[s[0] for s in SUBJECTS]) -@pytest.mark.parametrize("harmonic", [1, 3]) -def test_python_transform_matches_ansys(solved, label, phase_exprs, prefix, harmonic): - assert solved is not None, "solve returned no results" - needed = [*phase_exprs, "theta_el", f"{prefix}_d{harmonic}", f"{prefix}_q{harmonic}"] - missing = [k for k in needed if k not in solved] - assert not missing, f"missing from the extracted solution: {missing}" - - # (n_timesteps, 5), phases in order A..E; to_dq reads the phase count off that axis. - per_phase = np.column_stack([np.asarray(solved[k], dtype=float) for k in phase_exprs]) - # Ansys returns theta_el in DEGREES (its "- pi" evaluates as -180 in an angular - # context); transforms.to_dq takes radians. - theta_el = np.deg2rad(np.asarray(solved["theta_el"], dtype=float)) - - d_py, q_py = to_dq(per_phase, theta_el, harmonic) - d_aedt = np.asarray(solved[f"{prefix}_d{harmonic}"], dtype=float) - q_aedt = np.asarray(solved[f"{prefix}_q{harmonic}"], dtype=float) - - # rtol handles solver/report round-off; atol keeps near-zero samples from failing on - # relative error alone, scaled to the signal rather than to absolute units. - atol = 1e-6 * max(np.max(np.abs(d_aedt)), np.max(np.abs(q_aedt)), 1.0) - np.testing.assert_allclose(d_py, d_aedt, rtol=1e-6, atol=atol, err_msg=f"{label} d{harmonic} mismatch") - np.testing.assert_allclose(q_py, q_aedt, rtol=1e-6, atol=atol, err_msg=f"{label} q{harmonic} mismatch") diff --git a/tests/test_synrm_60s_torque_i3_zero.py b/tests/test_synrm_60s_torque_i3_zero.py deleted file mode 100644 index 406ff50..0000000 --- a/tests/test_synrm_60s_torque_i3_zero.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Goal: test 5f_60s performs the same with 3f_60s when Id3=Iq3=0 and the same rotor. - -Run explicitly with: - - pytest -m ansys tests/test_synrm_60s_torque_i3_zero.py - -Requires a running Ansys Electronics Desktop session and a free license seat. -""" - -import numpy as np -import pytest - -from machine_design.config import load_config -from machine_design.designs.design import Design as LiveDesign -from machine_design.optimization.generators import HacklGenerator_OneLambda -from machine_design.optimization.geometry import analyze_results -from motors.synrm_3f_60s import Computation as Computation3f -from motors.synrm_3f_60s import Geometry as Geometry3f -from motors.synrm_5f_60s import Computation as Computation5f -from motors.synrm_5f_60s import Geometry as Geometry5f - -pytestmark = pytest.mark.ansys - -AEDT_VERSION = load_config()["aedt_version"] -R_STATOR_END = 0.7 -OFFSET = 0.35 -SEEDS = (42, 43) -NUM_CORES = 1 -current_setpoints_3f = (2.5, 2.5) -current_setpoints_5f = (1.5, 1.5, 0.0, 0.0) - -Machines = [ - ("synrm_3f_60s", Geometry3f, Computation3f, current_setpoints_3f), - ("synrm_5f_60s", Geometry5f, Computation5f, current_setpoints_5f), -] - - -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 = Geometry3f() - dummy = LiveDesign(m2d=None, geometry=geometry, computation=Computation3f(geometry)) - 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 test_synrm_5f_60s_i3_zero_performs_same_with_3f_60s(tmp_path): - for seed in SEEDS: - barriers = _generate_one_lambda_barriers(seed) - - results = {} - for name, geometry_cls, computation_cls, current_setpoint in Machines: - geometry = geometry_cls() - computation = computation_cls(geometry) - design = LiveDesign.create( - f"CheckI3zero_{name}_{seed}", - "Design01", - str(tmp_path / f"{name}_{seed}.aedt"), - geometry, - computation, - version=AEDT_VERSION, - non_graphical=True, - new_desktop=False, - close_on_exit=False, - ) - try: - design.add_rotor() - for barrier in barriers: - design.add_rotor_barrier(barrier) - - torque = design.compute(*current_setpoint, NUM_CORES=NUM_CORES) - assert torque is not None, f"{name} seed {seed}: torque is None" - TorAvg, _, TorRippleRms = analyze_results(torque["Moving1.Torque"]) - results[name] = TorAvg - finally: - design.close_project() - - torque_3f = results["synrm_3f_60s"] - torque_5f = results["synrm_5f_60s"] - diff_pct = abs(torque_3f - torque_5f) / abs(torque_3f) * 100 - print(f"seed {seed}: 3f TorAvg={torque_3f:.4f} Nm, 5f(i3=0) TorAvg={torque_5f:.4f} Nm, diff={diff_pct:.2f}%") - assert diff_pct < 5, f"seed {seed}: 3f and 5f(i3=0) torque differ by {diff_pct:.2f}%, expected roughly comparable"