test_platform_registry.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. """Platform P2 regression tests: topology registry + adapter-driven executor.
  2. Covers (docs/PLATFORM_DESIGN_V2.md P2):
  3. 1. Topology registry: registration, lookup, parameter system, validation.
  4. 2. plan_schema topology validation integration.
  5. 3. Adapter registry + get_adapter("motorcad").
  6. 4. MotorCADTaskExecutor wired through get_adapter (metrics flattened to
  7. top level, FAILED points raise, _compute_metrics works) - via a fake
  8. adapter, so NO real Motor-CAD is launched.
  9. Run: python scripts/test_platform_registry.py
  10. Exit code 0 = all PASS. Pure ASCII source.
  11. """
  12. import os
  13. import sys
  14. ROOT = os.path.dirname(os.path.abspath(__file__))
  15. PROJ = os.path.dirname(ROOT)
  16. sys.path.insert(0, ROOT)
  17. sys.path.insert(0, PROJ)
  18. sys.path.insert(0, os.path.join(PROJ, "src"))
  19. sys.path.insert(0, os.path.join(PROJ, "web", "backend"))
  20. PASS = 0
  21. FAIL = 0
  22. def check(name, cond, detail=""):
  23. global PASS, FAIL
  24. if cond:
  25. PASS += 1
  26. print(" [PASS] %s%s" % (name, (" - " + detail) if detail else ""))
  27. else:
  28. FAIL += 1
  29. print(" [FAIL] %s%s" % (name, (" - " + detail) if detail else ""))
  30. def section(title):
  31. print("\n== %s ==" % title)
  32. # ---------------------------------------------------------------------------
  33. section("1. Topology registry")
  34. from afmcore.topology import ( # noqa: E402
  35. TOPOLOGY_REGISTRY,
  36. get_topology,
  37. get_topology_or_none,
  38. list_topologies,
  39. list_topology_codes,
  40. is_supported,
  41. is_active,
  42. param_names_for,
  43. validate_params,
  44. to_dict,
  45. register_topology,
  46. TopologyDefinition,
  47. )
  48. check("registry has 3 topologies", len(TOPOLOGY_REGISTRY) == 3)
  49. check("codes sorted", list_topology_codes() == ["DRSS", "SDSR", "SSSR"])
  50. sssr = get_topology("SSSR")
  51. check("SSSR label_zh", sssr.label_zh == "\u5355\u5b9a\u5b50\u5355\u8f6c\u5b50", sssr.label_zh)
  52. check("SSSR active", is_active("SSSR"))
  53. check("SSSR 37 params", len(sssr.all_params()) == 37, str(len(sssr.all_params())))
  54. check("SSSR 8 groups", len(sssr.param_system) == 8)
  55. check("SSSR sizing", "Airgap" in sssr.sizing_params and "Magnet_Thickness" in sssr.sizing_params)
  56. check("SSSR scan vars", set(sssr.default_scan_vars) >= {"Airgap", "Magnet_Thickness", "RMSCurrent"})
  57. check("SSSR single airgap", sssr.airgap_count == 1 and sssr.stator_count == 1 and sssr.rotor_count == 1)
  58. drss = get_topology("drss") # case-insensitive lookup
  59. check("DRSS planned + double airgap",
  60. drss.status == "planned" and drss.airgap_count == 2 and drss.rotor_count == 2)
  61. check("DRSS not active", not is_active("DRSS"))
  62. check("SDSR planned", get_topology("SDSR").status == "planned")
  63. check("is_supported('XXX') False", not is_supported("XXX"))
  64. check("get_topology_or_none unknown None", get_topology_or_none("XXX") is None)
  65. r = validate_params("SSSR", {"Airgap": 1.0, "RMSCurrent": 15.0, "Foo": 3})
  66. check("validate known", set(r["known"]) == {"Airgap", "RMSCurrent"})
  67. check("validate unknown", r["unknown"] == ["Foo"])
  68. check("validate supported", r["unsupported"] is False)
  69. r2 = validate_params("XXX", {"Airgap": 1.0})
  70. check("unknown topology -> unsupported", r2["unsupported"] is True)
  71. snap = to_dict()
  72. check("to_dict has 3", len(snap) == 3)
  73. check("to_dict SSSR params", len(snap["SSSR"]["params"]) == 37)
  74. # custom registration (idempotent / override)
  75. register_topology(TopologyDefinition(code="SSSR", label_zh="x", label_en="y", status="active"))
  76. check("re-register same code overrides", get_topology("SSSR").label_en == "y")
  77. register_topology(TopologyDefinition(code="SSSR", label_zh="\u5355\u5b9a\u5b50\u5355\u8f6c\u5b50",
  78. label_en="Single Stator Single Rotor", status="active",
  79. param_system=sssr.param_system,
  80. sizing_params=sssr.sizing_params,
  81. default_scan_vars=sssr.default_scan_vars))
  82. check("SSSR restored", get_topology("SSSR").label_en == "Single Stator Single Rotor")
  83. # ---------------------------------------------------------------------------
  84. section("2. plan_schema topology validation")
  85. from src.plan_schema import SimulationPlan # noqa: E402
  86. _, errs = SimulationPlan(topology="SSSR").validate()
  87. check("SSSR no topology error", all("topology not supported" not in e for e in errs), str(errs))
  88. _, errs = SimulationPlan(topology="TORUS").validate()
  89. check("TORUS rejected", any("topology not supported" in e for e in errs))
  90. _, errs = SimulationPlan(topology="DRSS").validate()
  91. check("DRSS accepted (registered)", all("topology not supported" not in e for e in errs))
  92. rt = SimulationPlan.from_dict(SimulationPlan(topology="SSSR").to_dict())
  93. check("serialization round-trip", rt.topology == "SSSR")
  94. # ---------------------------------------------------------------------------
  95. section("3. Adapter registry")
  96. from afmcore.adapters import ( # noqa: E402
  97. SimulationAdapter,
  98. register_adapter,
  99. get_adapter,
  100. registered_tools,
  101. )
  102. import afmcore.adapters.motorcad # noqa: F401,E402 (registers "motorcad")
  103. check("motorcad registered", "motorcad" in registered_tools())
  104. mc = get_adapter("motorcad", model_path="models/x.mot")
  105. check("get_adapter returns SimulationAdapter", isinstance(mc, SimulationAdapter))
  106. check("adapter tool meta", mc.tool_name == "motorcad" and mc.tool_label == "Motor-CAD (ANSYS)")
  107. try:
  108. get_adapter("maxwell")
  109. check("unknown tool raises", False)
  110. except KeyError:
  111. check("unknown tool raises", True)
  112. # ---------------------------------------------------------------------------
  113. section("4. MotorCADTaskExecutor through adapter (fake, no Motor-CAD)")
  114. CALLS = []
  115. class FakeAdapter(SimulationAdapter):
  116. tool_name = "fake"
  117. def __init__(self, **kw):
  118. super().__init__(**kw)
  119. self.points = 0
  120. def connect(self):
  121. CALLS.append("connect")
  122. def disconnect(self):
  123. CALLS.append("disconnect")
  124. def load_model(self, model_path):
  125. CALLS.append("load:" + model_path)
  126. def set_parameter(self, name, value):
  127. CALLS.append("set:%s" % name)
  128. def run_simulation(self, mode="electromagnetic"):
  129. CALLS.append("run")
  130. def extract_metrics(self, output_dir, tag=""):
  131. return {"metrics": {}, "status": "OK", "error": None, "raw_path": ""}
  132. def run_point(self, model_path, params=None, output_dir="output", tag=""):
  133. CALLS.append("run_point:" + str(tag))
  134. self.points += 1
  135. if tag == "fail":
  136. return {"metrics": {}, "status": "FAILED", "error": "boom", "raw_path": ""}
  137. return {"metrics": {"tavg_nm": 12.5 + self.points, "efficiency_pct": 93.1},
  138. "status": "OK", "error": None, "raw_path": "x", "solve_time_s": 1.5}
  139. register_adapter("fake", FakeAdapter)
  140. from task_executor import MotorCADTaskExecutor # noqa: E402
  141. ex = MotorCADTaskExecutor(model_path="models/x.mot", tool="fake", web_base_url="http://127.0.0.1:1")
  142. res = ex._run_simulation_point({"Airgap": 1.0}, 0)
  143. check("OK point flattened", res.get("tavg_nm") == 13.5 and res.get("status") == "OK", str(res))
  144. computed = ex._compute_metrics([res, res])
  145. check("compute_metrics mean", computed.get("tavg_nm_mean") == 13.5, str(computed))
  146. check("compute_metrics counts", computed["successful_points"] == 2 and computed["failed_points"] == 0)
  147. try:
  148. ex._run_simulation_point({"Airgap": 1.0}, "fail")
  149. check("FAILED point raises", False)
  150. except RuntimeError:
  151. check("FAILED point raises", True)
  152. # execute_task end-to-end with 3 points (no real backend calls needed; adapter
  153. # drives the simulation; requests is available so progress reports just 404)
  154. ex.execute_task({"task_id": "T-P2", "parameters": [{"Airgap": 1.0}, {"Airgap": 1.5}, {"Airgap": 2.0}]})
  155. check("execute_task created adapter", ex._adapter is not None)
  156. ex.cleanup()
  157. check("cleanup disconnected", "disconnect" in CALLS and ex._adapter is None)
  158. # ---------------------------------------------------------------------------
  159. print("\n================================")
  160. print("PASS: %d FAIL: %d" % (PASS, FAIL))
  161. sys.exit(0 if FAIL == 0 else 1)