Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 168 additions & 24 deletions notebooks/voila.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,50 @@
" f\"Return code: {result.returncode}\\n\"\n",
" f\"Stdout:\\n{result.stdout}\\n\"\n",
" f\"Stderr:\\n{result.stderr}\"\n",
" ) from error\n",
"\n",
"\n",
"def _build_report_cmd(*args):\n",
" \"\"\"Build the uvx command for the report-generation CLI.\"\"\"\n",
" if os.environ.get(\"SAR_PATTERN_VALIDATION_BACKEND_MODE\") == \"local\":\n",
" package_spec = str(PROJECT_ROOT)\n",
" else:\n",
" package_spec = GITHUB_PACKAGE_SPEC\n",
" return [\n",
" \"uvx\",\n",
" \"--no-cache\",\n",
" \"--from\",\n",
" package_spec,\n",
" \"sar-pattern-validation-report\",\n",
" *args,\n",
" ]\n",
"\n",
"\n",
"def run_report_generation(*args):\n",
" \"\"\"Run the report-generation CLI and return parsed JSON output.\"\"\"\n",
" cmd = _build_report_cmd(*args)\n",
"\n",
" env = os.environ.copy()\n",
" env[\"MPLBACKEND\"] = \"agg\"\n",
"\n",
" result = subprocess.run(cmd, capture_output=True, text=True, env=env)\n",
" _log_path = WORKSPACE_ROOT / \"system_state\" / \"voila_report.log\"\n",
" _log_path.parent.mkdir(exist_ok=True, parents=True)\n",
" with open(_log_path, \"w\") as _f:\n",
" _f.write(\n",
" f\"CMD: {' '.join(cmd)}\\n\\nSTDOUT:\\n{result.stdout}\\n\\nSTDERR:\\n{result.stderr}\\n\"\n",
" )\n",
" raw_output = result.stdout\n",
"\n",
" try:\n",
" return json.loads(raw_output)\n",
" except json.JSONDecodeError as error:\n",
" raise RuntimeError(\n",
" \"sar-pattern-validation-report did not return valid JSON.\\n\"\n",
" f\"Command: {' '.join(cmd)}\\n\"\n",
" f\"Return code: {result.returncode}\\n\"\n",
" f\"Stdout:\\n{result.stdout}\\n\"\n",
" f\"Stderr:\\n{result.stderr}\"\n",
" ) from error"
]
},
Expand Down Expand Up @@ -258,6 +302,9 @@
" WORKSPACE_ROOT / \"system_state\" / \"workflow_results.json\"\n",
" )\n",
" FILTERED_DB_CSV_PATH = str(WORKSPACE_ROOT / \"system_state\" / \"filtered_db.csv\")\n",
" WORKFLOW_RESULTS_JSON = str(\n",
" WORKSPACE_ROOT / \"system_state\" / \"workflow_results.json\"\n",
" )\n",
" REPORT_OUTPUT_DIR = str(WORKSPACE_ROOT / \"report\")\n",
" REPORT_TEMPLATE_DIR = str(PROJECT_ROOT / \"report_template\")"
]
Expand Down Expand Up @@ -793,6 +840,7 @@
" self.radio_button_grid._on_filter_changed = self._update_run_button\n",
"\n",
" self.workflow_results: WorkflowResults\n",
" self.report_generation_running = False\n",
"\n",
" # Thread control\n",
" self._progress_thread = None\n",
Expand Down Expand Up @@ -897,8 +945,11 @@
" self.reset_report_button.disabled = False\n",
" else:\n",
" self.reset_report_button.disabled = True\n",
" report_path_list = list(report_output_dir.glob(\"*.pdf\"))\n",
" if len(report_path_list) != 0:\n",
" workflow_results_json = Path(FilePaths.WORKFLOW_RESULTS_JSON.value)\n",
" if (\n",
" workflow_results_json.exists()\n",
" and not self.report_generation_running\n",
" ):\n",
" self.export_report_button.disabled = False\n",
" else:\n",
" self.export_report_button.disabled = True\n",
Expand Down Expand Up @@ -1076,19 +1127,6 @@
" *meas_area_args,\n",
" \"--noise_floor\",\n",
" f\"{self.noise_floor.value}\",\n",
" \"--report\",\n",
" \"--report_output_dir\",\n",
" f\"{FilePaths.REPORT_OUTPUT_DIR.value}\",\n",
" \"--report_template_dir\",\n",
" f\"{FilePaths.REPORT_TEMPLATE_DIR.value}\",\n",
" \"--report_antenna_type\",\n",
" f\"{_filter_options.antenna_type.value}\",\n",
" \"--report_frequency_mhz\",\n",
" f\"{int(_filter_options.frequency.value)}\",\n",
" \"--report_distance_mm\",\n",
" f\"{int(_filter_options.distance.value)}\",\n",
" \"--report_mass_g\",\n",
" f\"{int(_filter_options.mass.value)}\",\n",
" )\n",
"\n",
" self.logger.info(\"SAR Pattern Validation done.\")\n",
Expand Down Expand Up @@ -1489,21 +1527,127 @@
" ph.value = ph_data\n",
"\n",
" def handle_export_button_click(self, button: widgets.Button):\n",
" timestr = time.strftime(\"%Y%m%d-%H%M%S\")\n",
" \"\"\"Generate (or append) a 1-page report for the current results.\"\"\"\n",
" button.disabled = True\n",
" self.report_generation_running = True\n",
" self._clear_feedback_banner()\n",
"\n",
" report_output_dir = Path(FilePaths.REPORT_OUTPUT_DIR.value)\n",
" report_path = list(report_output_dir.glob(\"*.pdf\"))[0]\n",
" if not hasattr(self, \"workflow_results\") or self.workflow_results is None:\n",
" self._set_feedback_banner(\n",
" \"No workflow results available. Run 'Compare Patterns' first.\",\n",
" severity=\"error\",\n",
" )\n",
" return\n",
"\n",
" _filter_options = self.radio_button_grid.filter_options\n",
" if not _filter_options.antenna_type or not _filter_options.frequency:\n",
" self._set_feedback_banner(\n",
" \"Filter selections incomplete — cannot generate report.\",\n",
" severity=\"error\",\n",
" )\n",
" return\n",
"\n",
" try:\n",
" _measured_path = list(\n",
" Path(FilePaths.MEASURED_FILE_DIR.value).glob(\"*.csv\")\n",
" )[0]\n",
" _ref_path = str(\n",
" self.radio_button_grid.filtered_db[\n",
" DBColumnNames.FILE_PATH.value\n",
" ].to_list()[0]\n",
" )\n",
"\n",
" meas_area_args = []\n",
" if self.measurement_area_x.value and self.measurement_area_y.value:\n",
" meas_area_args = [\n",
" \"--measurement-area-x-mm\",\n",
" str(float(self.measurement_area_x.value)),\n",
" \"--measurement-area-y-mm\",\n",
" str(float(self.measurement_area_y.value)),\n",
" ]\n",
"\n",
" report_output = run_report_generation(\n",
" \"--workflow-results-json\",\n",
" FilePaths.WORKFLOW_RESULTS_JSON_PATH.value,\n",
" \"--measured-file-path\",\n",
" str(_measured_path),\n",
" \"--reference-file-path\",\n",
" _ref_path,\n",
" \"--power-level-dbm\",\n",
" str(self.power_level.value),\n",
" \"--noise-floor\",\n",
" str(self.noise_floor.value),\n",
" *meas_area_args,\n",
" \"--report-output-dir\",\n",
" FilePaths.REPORT_OUTPUT_DIR.value,\n",
" \"--report-template-dir\",\n",
" FilePaths.REPORT_TEMPLATE_DIR.value,\n",
" \"--antenna-type\",\n",
" str(_filter_options.antenna_type.value),\n",
" \"--frequency-mhz\",\n",
" str(int(_filter_options.frequency.value)),\n",
" \"--distance-mm\",\n",
" str(int(_filter_options.distance.value)),\n",
" \"--mass-g\",\n",
" str(int(_filter_options.mass.value)),\n",
" )\n",
"\n",
" outputs_dir = Path(os.environ.get(\"DY_SIDECAR_PATH_OUTPUTS\"))\n",
" output_1 = outputs_dir / \"output_1\"\n",
" outfile_path = output_1 / f\"report_{timestr}.pdf\"\n",
" if report_output.get(\"status\") != \"success\":\n",
" _msg = report_output.get(\"error\", {}).get(\n",
" \"message\", \"Report generation failed.\"\n",
" )\n",
" raise RuntimeError(_msg)\n",
"\n",
" shutil.copy2(report_path, outfile_path)\n",
" # Copy PDF to outputs directory as report.pdf\n",
" report_output_dir = Path(FilePaths.REPORT_OUTPUT_DIR.value)\n",
" pdf_list = list(report_output_dir.glob(\"*.pdf\"))\n",
" if pdf_list:\n",
" outputs_dir_env = os.environ.get(\"DY_SIDECAR_PATH_OUTPUTS\")\n",
" if outputs_dir_env:\n",
" outputs_dir = Path(outputs_dir_env)\n",
" output_1 = outputs_dir / \"output_1\"\n",
" output_1.mkdir(parents=True, exist_ok=True)\n",
" outfile_path = output_1 / \"report.pdf\"\n",
" shutil.copy2(pdf_list[0], outfile_path)\n",
"\n",
" self._set_feedback_banner(\n",
" \"Report page added successfully.\", severity=\"info\"\n",
" )\n",
"\n",
" except Exception as e:\n",
" self._set_feedback_banner(\n",
" f\"Report generation failed: {e}\", severity=\"error\"\n",
" )\n",
" self.logger.warning(f\"Report generation failed: {e}\")\n",
" finally:\n",
" button.disabled = False\n",
" self.report_generation_running = False\n",
"\n",
" def handle_reset_report_button_click(self, button: widgets.Button):\n",
" \"\"\"Show confirmation before deleting the current report.\"\"\"\n",
" button.disabled = True\n",
" self._clear_feedback_banner()\n",
"\n",
" report_output_dir = Path(FilePaths.REPORT_OUTPUT_DIR.value)\n",
" if report_output_dir.is_dir():\n",
" shutil.rmtree(report_output_dir)\n",
" if not report_output_dir.is_dir():\n",
" self._set_feedback_banner(\"No report to reset.\", severity=\"warning\")\n",
" return\n",
"\n",
" report_output_dir_inner = Path(FilePaths.REPORT_OUTPUT_DIR.value)\n",
" if report_output_dir_inner.is_dir():\n",
" shutil.rmtree(report_output_dir_inner)\n",
" # Also remove the exported PDF from outputs\n",
" outputs_dir_env = os.environ.get(\"DY_SIDECAR_PATH_OUTPUTS\")\n",
" if outputs_dir_env:\n",
" out_pdf = Path(outputs_dir_env) / \"output_1\" / \"report.pdf\"\n",
" if out_pdf.is_file():\n",
" out_pdf.unlink()\n",
" self._set_feedback_banner(\n",
" \"Report deleted. You can start a new report.\",\n",
" severity=\"info\",\n",
" )\n",
" self.feedback_banner.value = \"\"\n",
" button.disabled = False\n",
"\n",
" def create_maintenance_ui(self) -> widgets.HTML:\n",
" \"\"\"\n",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ e2e = [

[project.scripts]
sar-pattern-validation = "sar_pattern_validation.workflow_cli:main"
sar-pattern-validation-report = "sar_pattern_validation.report_cli:main"

[build-system]
requires = ["hatchling"]
Expand Down
Loading