test_adapters.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """P5-M5: unit tests for simulation tool adapters (maxwell / jmag mock).
  2. Covers: registry (register/get/listed), maxwell mock full pipeline,
  3. jmag mock full pipeline, tool differentiation (same params -> different
  4. metrics), set_parameter read-back verification, boundary (empty params /
  5. unknown tool / run before connect), anomaly (real-mode RuntimeError),
  6. and executor dynamic adapter import by tool name.
  7. All source is ASCII only. Run: python scripts/test_adapters.py
  8. exit 0 = PASS.
  9. """
  10. import os
  11. import sys
  12. import unittest
  13. _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  14. _SRC = os.path.join(_ROOT, "src")
  15. if _SRC not in sys.path:
  16. sys.path.insert(0, _SRC)
  17. from afmcore.adapters import ( # noqa: E402
  18. SimulationAdapter,
  19. get_adapter,
  20. register_adapter,
  21. registered_tools,
  22. )
  23. import afmcore.adapters.maxwell # noqa: E402,F401 (registers "maxwell")
  24. import afmcore.adapters.jmag # noqa: E402,F401 (registers "jmag")
  25. class TestAdapterRegistry(unittest.TestCase):
  26. def test_maxwell_registered(self):
  27. self.assertIn("maxwell", registered_tools())
  28. def test_jmag_registered(self):
  29. self.assertIn("jmag", registered_tools())
  30. def test_get_adapter_returns_instance(self):
  31. a = get_adapter("maxwell", model_path="/tmp/x.mot", mock=True)
  32. self.assertIsInstance(a, SimulationAdapter)
  33. self.assertEqual(a.tool_name, "maxwell")
  34. def test_unknown_tool_raises_keyerror(self):
  35. with self.assertRaises(KeyError):
  36. get_adapter("nonexistent_tool_xyz")
  37. def test_register_adapter_rejects_non_string(self):
  38. with self.assertRaises(ValueError):
  39. register_adapter("", SimulationAdapter)
  40. class TestMaxwellMockPipeline(unittest.TestCase):
  41. def setUp(self):
  42. self.adapter = get_adapter(
  43. "maxwell", model_path="/tmp/test.mot",
  44. output_dir="output/p5m5_test", mock=True,
  45. )
  46. def test_connect_disconnect(self):
  47. self.adapter.connect()
  48. self.assertTrue(self.adapter._connected)
  49. self.adapter.disconnect()
  50. self.assertFalse(self.adapter._connected)
  51. def test_load_model_clears_params(self):
  52. self.adapter._params["old"] = 1.0
  53. self.adapter.load_model("/tmp/new.mot")
  54. self.assertEqual(self.adapter._loaded_model, "/tmp/new.mot")
  55. self.assertEqual(self.adapter._params, {})
  56. def test_set_parameter_readback(self):
  57. self.adapter.connect()
  58. self.adapter.set_parameter("airgap_mm", 1.5)
  59. self.assertEqual(self.adapter._params["airgap_mm"], 1.5)
  60. def test_run_simulation_before_connect_raises(self):
  61. with self.assertRaises(RuntimeError):
  62. self.adapter.run_simulation("electromagnetic")
  63. def test_run_simulation_unsupported_mode_raises(self):
  64. self.adapter.connect()
  65. with self.assertRaises(ValueError):
  66. self.adapter.run_simulation("structural")
  67. def test_extract_metrics_deterministic(self):
  68. self.adapter.connect()
  69. self.adapter.set_parameter("airgap_mm", 1.0)
  70. self.adapter.set_parameter("magnet_thickness_mm", 5.0)
  71. ext = self.adapter.extract_metrics("output/p5m5_test", "t1")
  72. self.assertEqual(ext["status"], "OK")
  73. # tavg = 0.50*1.0 + 0.30*5.0 + 0.10 = 2.10
  74. self.assertAlmostEqual(ext["metrics"]["tavg_nm"], 2.10, places=4)
  75. self.assertAlmostEqual(ext["metrics"]["efficiency_pct"], 85.10, places=2)
  76. self.assertIn("winding_temp_c", ext["metrics"]) # thermal domain
  77. def test_run_point_full_pipeline(self):
  78. result = self.adapter.run_point(
  79. model_path="/tmp/test.mot",
  80. params={"airgap_mm": 1.0, "magnet_thickness_mm": 5.0},
  81. output_dir="output/p5m5_test",
  82. tag="full",
  83. )
  84. self.assertEqual(result["status"], "OK")
  85. self.assertIn("tavg_nm", result["metrics"])
  86. self.assertEqual(result["params"]["airgap_mm"], 1.0)
  87. def test_run_point_empty_params(self):
  88. result = self.adapter.run_point(
  89. model_path="/tmp/test.mot", params={},
  90. output_dir="output/p5m5_test", tag="empty",
  91. )
  92. self.assertEqual(result["status"], "OK")
  93. # all params default to 0 -> tavg = 0.10
  94. self.assertAlmostEqual(result["metrics"]["tavg_nm"], 0.10, places=4)
  95. def test_real_mode_raises(self):
  96. a = get_adapter("maxwell", model_path="/tmp/x.mot", mock=False)
  97. with self.assertRaises(RuntimeError):
  98. a.connect()
  99. class TestJMAGMockPipeline(unittest.TestCase):
  100. def setUp(self):
  101. self.adapter = get_adapter(
  102. "jmag", model_path="/tmp/test.jmag",
  103. output_dir="output/p5m5_test", mock=True,
  104. )
  105. def test_tool_label_and_domains(self):
  106. self.assertEqual(self.adapter.tool_label, "JMAG Designer (mock)")
  107. self.assertEqual(self.adapter.capability_domains, ("electromagnetic",))
  108. def test_extract_metrics_deterministic(self):
  109. self.adapter.connect()
  110. self.adapter.set_parameter("airgap_mm", 1.0)
  111. self.adapter.set_parameter("magnet_thickness_mm", 5.0)
  112. ext = self.adapter.extract_metrics("output/p5m5_test", "t1")
  113. # tavg = 0.45*1.0 + 0.32*5.0 + 0.12 = 2.17
  114. self.assertAlmostEqual(ext["metrics"]["tavg_nm"], 2.17, places=4)
  115. self.assertNotIn("winding_temp_c", ext["metrics"]) # jmag has no thermal domain
  116. def test_run_point_full_pipeline(self):
  117. result = self.adapter.run_point(
  118. model_path="/tmp/test.jmag",
  119. params={"airgap_mm": 2.0, "magnet_thickness_mm": 4.0},
  120. output_dir="output/p5m5_test", tag="full",
  121. )
  122. self.assertEqual(result["status"], "OK")
  123. # tavg = 0.45*2.0 + 0.32*4.0 + 0.12 = 0.90+1.28+0.12 = 2.30
  124. self.assertAlmostEqual(result["metrics"]["tavg_nm"], 2.30, places=4)
  125. def test_unsupported_mode_raises(self):
  126. self.adapter.connect()
  127. with self.assertRaises(ValueError):
  128. self.adapter.run_simulation("thermal")
  129. class TestToolDifferentiation(unittest.TestCase):
  130. """Same input params must produce different metrics for maxwell vs jmag."""
  131. def test_same_params_different_tavg(self):
  132. params = {"airgap_mm": 1.0, "magnet_thickness_mm": 5.0}
  133. mx = get_adapter("maxwell", mock=True)
  134. jm = get_adapter("jmag", mock=True)
  135. r_mx = mx.run_point("/tmp/x.mot", params, "output/p5m5_test", "diff")
  136. r_jm = jm.run_point("/tmp/x.jmag", params, "output/p5m5_test", "diff")
  137. self.assertNotEqual(r_mx["metrics"]["tavg_nm"], r_jm["metrics"]["tavg_nm"])
  138. self.assertNotEqual(r_mx["metrics"]["efficiency_pct"], r_jm["metrics"]["efficiency_pct"])
  139. class TestExecutorDynamicImport(unittest.TestCase):
  140. """MotorCADTaskExecutor must dynamically import the adapter matching tool."""
  141. def test_executor_tool_maxwell_creates_maxwell_adapter(self):
  142. sys.path.insert(0, _ROOT)
  143. from scripts.task_executor import MotorCADTaskExecutor
  144. ex = MotorCADTaskExecutor(
  145. web_base_url="http://127.0.0.1:9",
  146. model_path="/tmp/test.mot",
  147. tool="maxwell",
  148. enable_mock=False,
  149. )
  150. adapter = ex._ensure_adapter()
  151. self.assertEqual(adapter.tool_name, "maxwell")
  152. self.assertIn("maxwell", registered_tools())
  153. def test_executor_tool_jmag_creates_jmag_adapter(self):
  154. sys.path.insert(0, _ROOT)
  155. from scripts.task_executor import MotorCADTaskExecutor
  156. ex = MotorCADTaskExecutor(
  157. web_base_url="http://127.0.0.1:9",
  158. model_path="/tmp/test.jmag",
  159. tool="jmag",
  160. enable_mock=False,
  161. )
  162. adapter = ex._ensure_adapter()
  163. self.assertEqual(adapter.tool_name, "jmag")
  164. self.assertIn("jmag", registered_tools())
  165. def test_executor_tool_unknown_raises_keyerror(self):
  166. sys.path.insert(0, _ROOT)
  167. from scripts.task_executor import MotorCADTaskExecutor
  168. ex = MotorCADTaskExecutor(
  169. web_base_url="http://127.0.0.1:9",
  170. model_path="/tmp/test.mot",
  171. tool="nonexistent_tool_xyz",
  172. enable_mock=False,
  173. )
  174. with self.assertRaises(KeyError):
  175. ex._ensure_adapter()
  176. if __name__ == "__main__":
  177. unittest.main(verbosity=2)