From ad1a39ac6b8d893c3aced11b4ebdba35111faa84 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Mon, 6 Oct 2025 21:30:01 +0300 Subject: [PATCH] optimized bscan --- .../implementations/bscan_processor.py | 199 ++++++++++++------ 1 file changed, 136 insertions(+), 63 deletions(-) diff --git a/vna_system/core/processors/implementations/bscan_processor.py b/vna_system/core/processors/implementations/bscan_processor.py index 061834e..f815add 100644 --- a/vna_system/core/processors/implementations/bscan_processor.py +++ b/vna_system/core/processors/implementations/bscan_processor.py @@ -165,19 +165,6 @@ class BScanProcessor(BaseProcessor): Or: {"error": "..."} on failure. """ try: - # Skip B-scan processing for S21 mode - only works with S11 - # if vna_config and vna_config.get("mode") == "s21": - # return {"error": "B-scan only available in S11 mode"} - - # Choose calibrated data when provided - data_to_process = calibrated_data or sweep_data - - complex_data = self._get_complex_s11(data_to_process) - if complex_data is None or complex_data.size == 0: - logger.warning("No valid complex data for B-scan processing") - return {"error": "No valid complex data"} - - # Optional reference subtraction (latest stored reference in sweep history) reference_data: SweepData | None = None if self._config["open_air"] and self._sweep_history: latest_history = self._sweep_history[-1] @@ -223,18 +210,8 @@ class BScanProcessor(BaseProcessor): all_timestamps = [record["timestamp"] for record in self._plot_history] return { - "time_domain_data": analysis["time_data"].tolist(), # Latest sweep - "distance_data": analysis["distance"].tolist(), # Latest sweep - "frequency_range": analysis["freq_range"], - "reference_used": bool(self._config["open_air"] and reference_data is not None), - "axis_type": self._config["axis"], - "points_processed": int(complex_data.size), - "plot_history_count": len(self._plot_history), - # Full history data - "all_time_domain_data": all_time_domain, - "all_distance_data": all_distance, - "all_sweep_numbers": all_sweep_numbers, - "all_timestamps": all_timestamps, + **payload, + **history_payload, } except Exception as exc: # noqa: BLE001 @@ -396,47 +373,143 @@ class BScanProcessor(BaseProcessor): # Clear existing plot history to rebuild from scratch self._plot_history.clear() - # Process all sweeps in history with current config - last_processed = None - last_vna_config = {} - for entry in self._sweep_history: - sweep_data = entry["sweep_data"] - calibrated_data = entry["calibrated_data"] - vna_config = entry["vna_config"] + last_payload: dict[str, Any] | None = None + last_vna_config: dict[str, Any] = {} - # Use process_sweep to handle the processing logic - processed = self.process_sweep(sweep_data, calibrated_data, vna_config) - - # Skip if processing failed - if "error" not in processed: - last_processed = processed - last_vna_config = vna_config - - # Trim plot history if needed - if len(self._plot_history) > self._max_history: - self._plot_history = self._plot_history[-self._max_history:] - - logger.info("Recalculated B-scan with all history", - plot_records=len(self._plot_history), - sweep_records=len(self._sweep_history)) - - # Build result from last successful processing - if last_processed is None: - return None - - # Generate plotly config and wrap into ProcessedResult - plotly_conf = self.generate_plotly_config(last_processed, last_vna_config) - ui_params = self.get_ui_parameters() - - return ProcessedResult( - processor_id=self.processor_id, - timestamp=datetime.now().timestamp(), - data=last_processed, - plotly_config=plotly_conf, - ui_parameters=ui_params, - metadata=self._get_metadata(), + for entry in history_entries: + payload, plot_record = self._process_single_sweep( + entry["sweep_data"], + entry["calibrated_data"], + entry["vna_config"], + entry.get("reference_data"), ) + if "error" in payload: + continue + + self._append_plot_record(plot_record) + last_payload = payload + last_vna_config = entry.get("vna_config", {}) + + if last_payload is None: + return None + + history_payload = self._build_full_history_payload() + + combined_data = { + **last_payload, + **history_payload, + } + + logger.info( + "Recalculated B-scan with all history", + plot_records=history_payload["plot_history_count"], + sweep_records=len(history_entries), + ) + + plotly_conf = self.generate_plotly_config(combined_data, last_vna_config) + ui_params = self.get_ui_parameters() + + return ProcessedResult( + processor_id=self.processor_id, + timestamp=datetime.now().timestamp(), + data=combined_data, + plotly_config=plotly_conf, + ui_parameters=ui_params, + metadata=self._get_metadata(), + ) + + # ------------------------------------------------------------------------- + # History helpers + # ------------------------------------------------------------------------- + + def _process_single_sweep( + self, + sweep_data: SweepData, + calibrated_data: SweepData | None, + vna_config: dict[str, Any], + reference_data: SweepData | None, + ) -> tuple[dict[str, Any], dict[str, Any]]: + """Process a single sweep and produce payload + plot record.""" + + # Skip B-scan processing for S21 mode - only works with S11 + # if vna_config and vna_config.get("mode") == "s21": + # return {"error": "B-scan only available in S11 mode"}, {} + + data_to_process = calibrated_data or sweep_data + + complex_data = self._get_complex_s11(data_to_process) + if complex_data is None or complex_data.size == 0: + logger.warning("No valid complex data for B-scan processing") + return {"error": "No valid complex data"}, {} + + if self._config["open_air"] and reference_data is None: + logger.warning("Open air subtraction cannot be done: reference_data is None") + self._config["open_air"] = False + + if self._config["open_air"] and reference_data is not None: + reference_complex = self._get_complex_s11(reference_data) + if reference_complex is not None and reference_complex.size: + complex_data = self._subtract_reference(complex_data, reference_complex) + logger.debug("Applied open-air reference subtraction") + + self._update_frequency_ranges(vna_config) + + analysis = self._perform_data_analysis(complex_data, vna_config) + if analysis is None: + logger.warning("Data analysis failed") + return {"error": "Data analysis failed"}, {} + + time_data = analysis["time_data"].tolist() + distance_data = analysis["distance"].tolist() + freq_range = analysis["freq_range"] + + plot_record = { + "time_domain_data": time_data, + "distance_data": distance_data, + "sweep_number": sweep_data.sweep_number, + "timestamp": sweep_data.timestamp, + "frequency_range": freq_range, + } + + payload = { + "time_domain_data": time_data, + "distance_data": distance_data, + "frequency_range": freq_range, + "reference_used": bool(self._config["open_air"] and reference_data is not None), + "axis_type": self._config["axis"], + "points_processed": int(complex_data.size), + } + + return payload, plot_record + + def _append_plot_record(self, plot_record: dict[str, Any]) -> None: + """Store a plot record in history with trimming.""" + if not plot_record: + return + + with self._lock: + self._plot_history.append(plot_record) + if len(self._plot_history) > self._max_history: + self._plot_history = self._plot_history[-self._max_history :] + + def _build_full_history_payload(self) -> dict[str, Any]: + """Assemble cached history into JSON-friendly lists.""" + with self._lock: + all_time_domain = [record["time_domain_data"] for record in self._plot_history] + all_distance = [record["distance_data"] for record in self._plot_history] + all_sweep_numbers = [record["sweep_number"] for record in self._plot_history] + all_timestamps = [record["timestamp"] for record in self._plot_history] + count = len(self._plot_history) + + return { + "all_time_domain_data": all_time_domain, + "all_distance_data": all_distance, + "all_sweep_numbers": all_sweep_numbers, + "all_timestamps": all_timestamps, + "plot_history_count": count, + } + # ------------------------------------------------------------------------- # Low-level helpers # -------------------------------------------------------------------------