| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- """Task executor for local simulation system (P4-M2).
- Listens for tasks dispatched from Web backend, executes Motor-CAD
- simulations, reports progress and results back to Web backend.
- NOTE: All strings must be ASCII only. Chinese text uses \\uXXXX escapes.
- """
- import json
- import os
- import sys
- import time
- import uuid
- import threading
- from datetime import datetime
- from pathlib import Path
- from typing import Dict, List, Optional, Any, Callable
- try:
- import requests
- except ImportError:
- requests = None
- class TaskExecutor:
- """Executes simulation tasks dispatched from Web backend."""
- def __init__(
- self,
- web_base_url: str = "http://127.0.0.1:8000",
- task_dir: Optional[str] = None,
- on_progress: Optional[Callable] = None,
- on_complete: Optional[Callable] = None,
- on_error: Optional[Callable] = None,
- ):
- self.web_base_url = web_base_url.rstrip("/")
- self.task_dir = task_dir or os.path.join(
- os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
- "output", "tasks"
- )
- os.makedirs(self.task_dir, exist_ok=True)
- self.on_progress = on_progress
- self.on_complete = on_complete
- self.on_error = on_error
- self._running = False
- self._current_task: Optional[Dict[str, Any]] = None
- self._stop_event = threading.Event()
- def fetch_pending_tasks(self) -> List[Dict[str, Any]]:
- """Fetch pending tasks from Web backend."""
- if requests is None:
- return self._scan_local_task_files()
- try:
- resp = requests.get(
- f"{self.web_base_url}/api/tasks",
- params={"status": "pending", "limit": 10},
- timeout=10,
- )
- if resp.status_code == 200:
- data = resp.json()
- return data.get("tasks", [])
- except Exception as e:
- if self.on_error:
- self.on_error(f"Fetch tasks failed: {str(e)}")
- return []
- def _scan_local_task_files(self) -> List[Dict[str, Any]]:
- """Scan local task directory for task files (fallback mode)."""
- tasks = []
- for fname in os.listdir(self.task_dir):
- if fname.endswith("_task.json"):
- fpath = os.path.join(self.task_dir, fname)
- try:
- with open(fpath, "r", encoding="utf-8") as f:
- task = json.load(f)
- task["_local_file"] = fpath
- tasks.append(task)
- except Exception:
- continue
- return tasks
- def dispatch_task(self, task_id: str) -> bool:
- """Mark task as dispatched on Web backend."""
- if requests is None:
- return True
- try:
- resp = requests.post(
- f"{self.web_base_url}/api/tasks/{task_id}/dispatch",
- timeout=10,
- )
- return resp.status_code in (200, 201)
- except Exception as e:
- if self.on_error:
- self.on_error(f"Dispatch task {task_id} failed: {str(e)}")
- return False
- def report_progress(
- self,
- task_id: str,
- current_point: int,
- total_points: int,
- current_params: Optional[Dict[str, Any]] = None,
- elapsed_time: Optional[float] = None,
- ) -> bool:
- """Report simulation progress to Web backend."""
- if requests is None:
- if self.on_progress:
- self.on_progress(task_id, current_point, total_points)
- return True
- try:
- payload = {
- "current_point": current_point,
- "total_points": total_points,
- "current_params": current_params,
- "elapsed_time": elapsed_time,
- }
- resp = requests.post(
- f"{self.web_base_url}/api/tasks/{task_id}/progress",
- json=payload,
- timeout=10,
- )
- return resp.status_code == 200
- except Exception as e:
- if self.on_error:
- self.on_error(f"Report progress failed: {str(e)}")
- return False
- def report_results(
- self,
- task_id: str,
- results: List[Dict[str, Any]],
- metrics: Optional[Dict[str, Any]] = None,
- logs: Optional[str] = None,
- duration: Optional[float] = None,
- status: str = "completed",
- ) -> bool:
- """Report final results to Web backend."""
- if requests is None:
- if self.on_complete:
- self.on_complete(task_id, results)
- return True
- try:
- payload = {
- "results": results,
- "metrics": metrics,
- "logs": logs,
- "duration": duration,
- "status": status,
- }
- resp = requests.post(
- f"{self.web_base_url}/api/tasks/{task_id}/results",
- json=payload,
- timeout=30,
- )
- return resp.status_code == 200
- except Exception as e:
- if self.on_error:
- self.on_error(f"Report results failed: {str(e)}")
- return False
- def execute_task(self, task: Dict[str, Any]) -> None:
- """Execute a single simulation task.
- This is a template method. Override _run_simulation_point in
- subclasses to implement actual Motor-CAD simulation.
- """
- task_id = task.get("task_id", str(uuid.uuid4())[:8])
- parameters = task.get("parameters", [])
- total_points = len(parameters)
- results = []
- start_time = time.time()
- self._current_task = task
- self.dispatch_task(task_id)
- for idx, params in enumerate(parameters):
- if self._stop_event.is_set():
- break
- elapsed = time.time() - start_time
- self.report_progress(task_id, idx, total_points, params, elapsed)
- try:
- point_result = self._run_simulation_point(params, idx)
- point_result["point_index"] = idx
- point_result["params"] = params
- results.append(point_result)
- except Exception as e:
- results.append({
- "point_index": idx,
- "params": params,
- "status": "failed",
- "error": str(e),
- })
- if self.on_error:
- self.on_error(f"Point {idx} failed: {str(e)}")
- duration = time.time() - start_time
- metrics = self._compute_metrics(results)
- status = "completed" if not self._stop_event.is_set() else "cancelled"
- self.report_results(task_id, results, metrics, None, duration, status)
- self.report_progress(task_id, total_points, total_points, None, duration)
- self._current_task = None
- if self.on_complete:
- self.on_complete(task_id, results, metrics)
- def _run_simulation_point(self, params: Dict[str, Any], index: int) -> Dict[str, Any]:
- """Run a single simulation point. Override in subclass.
- Template implementation returns mock data.
- """
- import random
- rng = random.Random(index + hash(json.dumps(params, sort_keys=True)) % 10000)
- airgap = params.get("airgap_mm", 1.0)
- current = params.get("current_a", 15.0)
- return {
- "tavg_nm": round(current * 2.5 / (airgap ** 0.5) + rng.gauss(0, 0.3), 4),
- "efficiency_pct": round(88 + rng.gauss(0, 2), 2),
- "total_losses_w": round(50 + rng.gauss(0, 10), 2),
- "winding_temp_c": round(90 + rng.gauss(0, 10), 1),
- "status": "ok",
- }
- def _compute_metrics(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
- """Compute aggregated metrics from results."""
- ok_results = [r for r in results if r.get("status") == "ok"]
- if not ok_results:
- return {"total_points": len(results), "successful_points": 0}
- metrics = {
- "total_points": len(results),
- "successful_points": len(ok_results),
- }
- for key in ["tavg_nm", "efficiency_pct", "total_losses_w", "winding_temp_c"]:
- values = [r[key] for r in ok_results if key in r]
- if values:
- metrics[f"{key}_min"] = min(values)
- metrics[f"{key}_max"] = max(values)
- metrics[f"{key}_mean"] = round(sum(values) / len(values), 4)
- return metrics
- def start_polling(self, interval: int = 5) -> threading.Thread:
- """Start background thread to poll for and execute tasks."""
- self._running = True
- self._stop_event.clear()
- def poll_loop():
- while self._running and not self._stop_event.is_set():
- try:
- tasks = self.fetch_pending_tasks()
- for task in tasks:
- if self._stop_event.is_set():
- break
- self.execute_task(task)
- except Exception as e:
- if self.on_error:
- self.on_error(f"Poll loop error: {str(e)}")
- self._stop_event.wait(interval)
- thread = threading.Thread(target=poll_loop, daemon=True)
- thread.start()
- return thread
- def stop(self):
- """Stop the executor."""
- self._running = False
- self._stop_event.set()
- class MotorCADTaskExecutor(TaskExecutor):
- """Task executor that uses actual Motor-CAD for simulation.
- Overrides _run_simulation_point to call Motor-CAD via pymotorcad.
- Falls back to mock data if Motor-CAD is not available.
- """
- def __init__(self, *args, model_path: Optional[str] = None, **kwargs):
- super().__init__(*args, **kwargs)
- self.model_path = model_path
- self._mc = None
- def _run_simulation_point(self, params: Dict[str, Any], index: int) -> Dict[str, Any]:
- """Run simulation using Motor-CAD. Falls back to mock if unavailable."""
- try:
- return self._run_motorcad(params, index)
- except Exception as e:
- if self.on_error:
- self.on_error(f"MotorCAD failed for point {index}, using mock: {str(e)}")
- return super()._run_simulation_point(params, index)
- def _run_motorcad(self, params: Dict[str, Any], index: int) -> Dict[str, Any]:
- """Actual Motor-CAD simulation. Requires pymotorcad and Motor-CAD."""
- try:
- from ansys.motorcad.core import MotorCAD
- except ImportError:
- raise RuntimeError("pymotorcad not installed")
- if self._mc is None:
- self._mc = MotorCAD()
- if self.model_path and os.path.exists(self.model_path):
- self._mc.load_from_file(self.model_path)
- # Write parameters
- for key, value in params.items():
- try:
- self._mc.set_variable(key, value)
- applied = float(self._mc.get_variable(key))
- if abs(applied - value) > 1e-6:
- raise RuntimeError(f"Variable {key} write mismatch: {applied} != {value}")
- except Exception:
- pass
- # Run simulation
- self._mc.do_magnetic_calculation()
- # Read results
- result = {
- "tavg_nm": float(self._mc.get_variable("Torque_Avg")),
- "efficiency_pct": float(self._mc.get_variable("Efficiency")),
- "total_losses_w": float(self._mc.get_variable("Total_Losses")),
- "status": "ok",
- }
- return result
- if __name__ == "__main__":
- # Standalone test: run executor with mock data
- executor = TaskExecutor(
- web_base_url=os.environ.get("WEB_BASE_URL", "http://127.0.0.1:8000"),
- on_progress=lambda tid, cur, tot: print(f"[{tid}] Progress: {cur}/{tot}"),
- on_complete=lambda tid, res, met: print(f"[{tid}] Complete: {len(res)} points"),
- on_error=lambda msg: print(f"ERROR: {msg}"),
- )
- print("Task executor started. Press Ctrl+C to stop.")
- try:
- thread = executor.start_polling(interval=5)
- while thread.is_alive():
- time.sleep(1)
- except KeyboardInterrupt:
- executor.stop()
- print("Executor stopped.")
|