fixed problems

This commit is contained in:
Ayzen
2025-10-06 21:17:02 +03:00
parent c98b96dd0c
commit 85ac55cf1e
6 changed files with 142 additions and 82 deletions

View File

@ -1,4 +1,4 @@
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict, is_dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -324,7 +324,7 @@ class BaseProcessor:
{ {
"sweep_data": sweep_data, "sweep_data": sweep_data,
"calibrated_data": calibrated_data, "calibrated_data": calibrated_data,
"vna_config": asdict(vna_config) if vna_config is not None else {}, "vna_config": self._snapshot_vna_config(vna_config),
"reference_data": reference_data, "reference_data": reference_data,
"timestamp": datetime.now().timestamp(), "timestamp": datetime.now().timestamp(),
} }
@ -562,25 +562,40 @@ class BaseProcessor:
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# History Export/Import # History Export/Import
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
def _make_json_serializable(self, obj: Any) -> Any: @staticmethod
""" def _snapshot_vna_config(vna_config: ConfigPreset | dict[str, Any] | None) -> dict[str, Any]:
Recursively convert non-serializable objects to JSON-compatible types. """Create a JSON-friendly copy of VNA preset data."""
if vna_config is None:
return {}
Handles Enums, custom objects, etc. from enum import Enum # Local import to avoid module-level dependency
"""
from enum import Enum
if isinstance(obj, Enum): if isinstance(vna_config, dict):
return obj.value source_items = vna_config.items()
elif isinstance(obj, dict): elif is_dataclass(vna_config):
return {key: self._make_json_serializable(value) for key, value in obj.items()} source_items = asdict(vna_config).items()
elif isinstance(obj, (list, tuple)):
return [self._make_json_serializable(item) for item in obj]
elif hasattr(obj, '__dict__'):
# Custom object - try to serialize its dict
return self._make_json_serializable(obj.__dict__)
else: else:
return obj try:
source_items = vars(vna_config).items()
except TypeError:
return {}
sanitized: dict[str, Any] = {}
for key, value in source_items:
if isinstance(value, Enum):
sanitized[key] = value.value
elif hasattr(value, "item") and callable(value.item):
try:
sanitized[key] = value.item()
continue
except (TypeError, ValueError):
pass
elif hasattr(value, "tolist") and callable(value.tolist):
sanitized[key] = value.tolist()
continue
sanitized[key] = value
return sanitized
def export_history_data(self) -> list[dict[str, Any]]: def export_history_data(self) -> list[dict[str, Any]]:
""" """
@ -598,16 +613,13 @@ class BaseProcessor:
calibrated_data = entry["calibrated_data"] calibrated_data = entry["calibrated_data"]
reference_data = entry.get("reference_data") reference_data = entry.get("reference_data")
# Convert vna_config to fully serializable format (handles Enums)
vna_config = self._make_json_serializable(entry.get("vna_config", {}))
exported.append({ exported.append({
"sweep_number": sweep_data.sweep_number if sweep_data else None, "sweep_number": sweep_data.sweep_number if sweep_data else None,
"timestamp": entry.get("timestamp"), "timestamp": float(entry.get("timestamp")) if entry.get("timestamp") is not None else None,
"sweep_points": sweep_data.points if sweep_data else [], "sweep_points": self._points_to_list(getattr(sweep_data, "points", [])),
"calibrated_points": calibrated_data.points if calibrated_data else [], "calibrated_points": self._points_to_list(getattr(calibrated_data, "points", [])),
"reference_points": reference_data.points if reference_data else [], "reference_points": self._points_to_list(getattr(reference_data, "points", [])),
"vna_config": vna_config, "vna_config": self._snapshot_vna_config(entry.get("vna_config")),
}) })
return exported return exported
@ -672,6 +684,33 @@ class BaseProcessor:
logger.info("History imported", processor_id=self.processor_id, records=len(history_data)) logger.info("History imported", processor_id=self.processor_id, records=len(history_data))
@staticmethod
def _points_to_list(points: Any) -> list[tuple[float, float]]:
"""Convert sweep points into plain Python floats."""
result: list[tuple[float, float]] = []
if not points:
return result
for point in points:
if isinstance(point, dict):
real = point.get("real")
imag = point.get("imag")
if real is None:
real = point.get("r")
if imag is None:
imag = point.get("i")
elif isinstance(point, (list, tuple)) and len(point) >= 2:
real, imag = point[0], point[1]
else:
continue
try:
result.append((float(real), float(imag)))
except (TypeError, ValueError):
continue
return result
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Utilities # Utilities
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #

View File

@ -54,7 +54,7 @@ class CalibrationProcessor:
else: else:
raise ValueError(f"Unsupported measurement mode: {calibration_set.preset.mode}") raise ValueError(f"Unsupported measurement mode: {calibration_set.preset.mode}")
return [(z.real, z.imag) for z in calibrated] return [(float(z.real), float(z.imag)) for z in calibrated]
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Helpers # Helpers

View File

@ -1,7 +1,7 @@
{ {
"open_air": false, "open_air": false,
"axis": "abs", "axis": "phase",
"cut": 0.292, "cut": 0.266,
"max": 2.3, "max": 2.3,
"gain": 0.3, "gain": 0.3,
"start_freq": 100.0, "start_freq": 100.0,

View File

@ -1,10 +1,11 @@
import numpy as np import numpy as np
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from dataclasses import asdict
from vna_system.core.acquisition.sweep_buffer import SweepData from vna_system.core.acquisition.sweep_buffer import SweepData
from vna_system.core.logging.logger import get_component_logger from vna_system.core.logging.logger import get_component_logger
from vna_system.core.processors.base_processor import BaseProcessor, UIParameter from vna_system.core.processors.base_processor import BaseProcessor, UIParameter
from vna_system.core.settings.preset_manager import VNAMode
logger = get_component_logger(__file__) logger = get_component_logger(__file__)
@ -112,7 +113,13 @@ class MagnitudeProcessor(BaseProcessor):
autoscale: bool = processed_data["autoscale"] autoscale: bool = processed_data["autoscale"]
# Determine the parameter type from preset mode # Determine the parameter type from preset mode
parameter_type = vna_config["mode"].value.upper() # Convert "s11" -> "S11", "s21" -> "S21" mode = vna_config.get("mode")
if isinstance(mode, VNAMode):
parameter_type = mode.value.upper()
elif isinstance(mode, str):
parameter_type = mode.upper()
else:
parameter_type = "UNKNOWN"
# Convert Hz to GHz for x-axis # Convert Hz to GHz for x-axis
freqs_ghz = [f / 1e9 for f in freqs] freqs_ghz = [f / 1e9 for f in freqs]
@ -280,4 +287,3 @@ class MagnitudeProcessor(BaseProcessor):
logger.info("Adjusted y_max to maintain y_max > y_min", logger.info("Adjusted y_max to maintain y_max > y_min",
y_min=self._config["y_min"], y_min=self._config["y_min"],
y_max=self._config["y_max"]) y_max=self._config["y_max"])

View File

@ -2,6 +2,7 @@ from pathlib import Path
from typing import Any, Callable from typing import Any, Callable
import threading import threading
from enum import Enum
from vna_system.core.logging.logger import get_component_logger from vna_system.core.logging.logger import get_component_logger
from vna_system.core.processors.base_processor import BaseProcessor, ProcessedResult from vna_system.core.processors.base_processor import BaseProcessor, ProcessedResult
@ -196,6 +197,29 @@ class ProcessorManager:
logger.error("History load error", processor_id=processor_id, error=repr(exc)) logger.error("History load error", processor_id=processor_id, error=repr(exc))
raise raise
def build_processor_state(self, processor_id: str) -> dict[str, Any]:
"""Return a JSON-ready snapshot of processor state and current result."""
processor = self.get_processor(processor_id)
if processor is None:
raise ValueError(f"Processor {processor_id} not found")
state_payload = self._json_ready(processor.get_full_state())
result = processor.recalculate()
current_data = None
if result is not None:
current_data = {
"data": self._json_ready(result.data),
"plotly_config": self._json_ready(result.plotly_config),
"timestamp": float(result.timestamp),
}
return {
"type": "processor_state",
"processor_id": processor_id,
"state": state_payload,
"current_data": current_data,
}
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Runtime control # Runtime control
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
@ -268,12 +292,15 @@ class ProcessorManager:
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
# Calibration # Calibration
# --------------------------------------------------------------------- # # --------------------------------------------------------------------- #
def _apply_calibration(self, sweep_data: SweepData) -> SweepData: def _apply_calibration(self, sweep_data: SweepData | None) -> SweepData | None:
""" """
Apply calibration to the sweep when a complete set is available. Apply calibration to the sweep when a complete set is available.
Returns the original sweep on failure or when no calibration is present. Returns the original sweep on failure or when no calibration is present.
""" """
if sweep_data is None:
return None
try: try:
calib_set = self.settings_manager.get_current_calibration() calib_set = self.settings_manager.get_current_calibration()
if calib_set and calib_set.is_complete(): if calib_set and calib_set.is_complete():
@ -319,3 +346,37 @@ class ProcessorManager:
logger.info("Default processors registered", count=len(self._processors)) logger.info("Default processors registered", count=len(self._processors))
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("Failed to register default processors", error=repr(exc)) logger.error("Failed to register default processors", error=repr(exc))
# --------------------------------------------------------------------- #
# JSON coercion helpers
# --------------------------------------------------------------------- #
@staticmethod
def _json_ready(obj: Any) -> Any:
"""Convert numpy-like objects to JSON primitives recursively."""
if obj is None or isinstance(obj, (str, bool, int, float)):
return obj
if isinstance(obj, Enum):
return obj.value
if isinstance(obj, dict):
return {key: ProcessorManager._json_ready(value) for key, value in obj.items()}
if isinstance(obj, (list, tuple, set)):
return [ProcessorManager._json_ready(item) for item in obj]
item_method = getattr(obj, "item", None)
if callable(item_method):
try:
return item_method()
except (TypeError, ValueError):
pass
tolist_method = getattr(obj, "tolist", None)
if callable(tolist_method):
try:
return ProcessorManager._json_ready(tolist_method())
except (TypeError, ValueError):
pass
return obj

View File

@ -213,58 +213,12 @@ class ProcessorWebSocketHandler:
return return
try: try:
processor = self.processor_manager.get_processor(processor_id) response = self.processor_manager.build_processor_state(processor_id)
if processor is None:
await self._send_error(websocket, f"Processor {processor_id} not found")
return
# Get full state with sweep history
full_state = processor.get_full_state()
# Get current result to include data and plotly_config
current_result = processor.recalculate()
response = {
"type": "processor_state",
"processor_id": processor_id,
"state": full_state,
"current_data": {
"data": current_result.data if current_result else {},
"plotly_config": current_result.plotly_config if current_result else {},
"timestamp": current_result.timestamp if current_result else datetime.now().timestamp(),
} if current_result else None,
}
await websocket.send_text(json.dumps(response)) await websocket.send_text(json.dumps(response))
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.error("Error getting processor state", processor_id=processor_id, error=repr(exc)) logger.error("Error getting processor state", processor_id=processor_id, error=repr(exc))
await self._send_error(websocket, f"Error getting processor state: {exc}") await self._send_error(websocket, f"Error getting processor state: {exc}")
# --------------------------------------------------------------------- #
# Outbound helpers
# --------------------------------------------------------------------- #
def _make_json_serializable(self, obj: Any) -> Any:
"""
Recursively convert non-serializable objects to JSON-compatible types.
Handles numpy types, Enums, custom objects, etc.
"""
import numpy as np
from enum import Enum
if isinstance(obj, (np.integer, np.floating)):
return obj.item() # Convert numpy scalar to Python type
elif isinstance(obj, np.ndarray):
return obj.tolist() # Convert numpy array to list
elif isinstance(obj, Enum):
return obj.value
elif isinstance(obj, dict):
return {key: self._make_json_serializable(value) for key, value in obj.items()}
elif isinstance(obj, (list, tuple)):
return [self._make_json_serializable(item) for item in obj]
elif hasattr(obj, '__dict__'):
return self._make_json_serializable(obj.__dict__)
else:
return obj
def _result_to_message(self, processor_id: str, result: ProcessedResult) -> dict[str, Any]: def _result_to_message(self, processor_id: str, result: ProcessedResult) -> dict[str, Any]:
""" """
Convert a `ProcessedResult` into a lightweight JSON-serializable message for broadcasting. Convert a `ProcessedResult` into a lightweight JSON-serializable message for broadcasting.
@ -278,7 +232,7 @@ class ProcessorWebSocketHandler:
"timestamp": result.timestamp, "timestamp": result.timestamp,
"plotly_config": result.plotly_config, "plotly_config": result.plotly_config,
"ui_parameters": [asdict(param) for param in result.ui_parameters], "ui_parameters": [asdict(param) for param in result.ui_parameters],
"metadata": self._make_json_serializable(result.metadata), "metadata": result.metadata,
} }
async def _send_error( async def _send_error(