diff --git a/report_template/main.tex b/report_template/main.tex index b3b562d..016a96a 100644 --- a/report_template/main.tex +++ b/report_template/main.tex @@ -8,15 +8,13 @@ \usepackage{subcaption} \usepackage{placeins} \usepackage{xspace} -\IfFileExists{datetime.sty}{% - \usepackage[us,24hr]{datetime}% -}{} -\IfFileExists{helvet.sty}{% - \usepackage{helvet}% - \renewcommand{\familydefault}{\sfdefault}% -}{} +\usepackage[us,24hr]{datetime} +% \usepackage{helvet} +% \renewcommand{\familydefault}{\sfdefault} \usepackage{fp} \usepackage{footnote} +\usepackage{ifthen} +\usepackage{pgf} \makesavenoteenv{tabular} % input variables @@ -29,6 +27,16 @@ \newcommand{\mass}{10} % grams \newcommand{\powerlevel}{16.5} % dBm +\newcommand{\uncantenna}{} % percent +\ifthenelse{\equal{\antennatype}{VPIFA}}{% + \renewcommand{\uncantenna}{14.3}% +}{% + \renewcommand{\uncantenna}{9.4}% +} + +\pgfmathsetmacro{\scaletolerance}{sqrt(0.25^2 + (\uncantenna/100)^2)} +\FPeval{\scaletol}{round(100*\scaletolerance, 1)} + \newcommand{\pssarref}{24.71} % W/kg \newcommand{\pssarmeas}{1.31} % W/kg \FPeval{\pssarscaled}{round(\pssarmeas * 10^((30-\powerlevel)/10),2)} @@ -53,10 +61,11 @@ \newcommand{\gammastatement}{The pattern validation passes because $\Gamma (x_e,y_e)~\leq~1.0$ at all locations of the measured sSAR distribution, compared to the reference, } \fi +\newcommand{\scalestatementpost}{the tolerance of $\pm~\sqrt{\errscaletolerance~\%^2 + U_{r,m}^2} = \scaletol~\%$, where $U_{r,m}^2$ = \uncantenna~\% ~for this antenna} \FPifgt{\errscaleabs}{\errscaletolerance}% - \newcommand{\scalestatement}{The scaling error for the psSAR is outside the $\pm$~\errscaletolerance~\% criteria.} + \newcommand{\scalestatement}{The scaling error for the psSAR is outside \scalestatementpost.} \else - \newcommand{\scalestatement}{The scaling error for the psSAR is within the $\pm$~\errscaletolerance~\% criteria.} + \newcommand{\scalestatement}{The scaling error for the psSAR is within \scalestatementpost.} \fi \title{SAR Pattern Validation Report} @@ -88,7 +97,8 @@ \subsection*{Measured sSAR Parameters} \subsection*{Results} \gammastatement ~according to the Gamma criterion described in IEC/IEEE PAS 62209-5 -with $\Delta D~=~$\deltadose, $\Delta d$~=~\deltadist. \scalestatement +with $\Delta D~=~$\deltadose, $\Delta d$~=~\deltadist. +\scalestatement \begin{figure}[h!] \centering @@ -108,3 +118,31 @@ \subsection*{Results} % \caption*{} \end{figure} \end{document} + +% \maketitle +% \thispagestyle{empty} + +% \section*{Background} + +% This report documents the validation of a measured spatially-averaged SAR (sSAR) pattern compared to a reference distribution. A reference software implementation is provided, where a measured sSAR distribution is uploaded and the input parameters are chosen (antenna type, frequency, distance, and averaging mass). The software evaluates the Gamma match and determines whether the sSAR distribution is compliant, according to the criteria. The software also creates plots to show the measured and reference SAR distributions, the registration overlay, the gamma index map and the pass/fail map. + +% Validation of spatial SAR distributions is performed using the Gamma method developed by Low and Dempsey~\cite{low-dempsey} to compare measured spatial SAR distributions, $sSAR_{en}(x_e,y_e)$, with corresponding reference distributions, $sSAR_{rn}(x_r,y_r)$, each normalized to its peak value. The Gamma method combines two criteria, SAR magnitude difference and spatial distance-to-agreement, combining these two dimensions (after normalization by their respective tolerances $\Delta D$ and $\Delta d$) into a single Euclidian distance norm, $\Gamma$, which is used to identify corresponding points: + + +% $sSARen(x_e,y_e)$ and $sSARrn(x_r,y_r)$ are both extracted at the boundary between the phantom shell and lossy medium. Before normalization, masking is performed to select only those values above 0.1 W/kg or twice the noise floor of the system, whichever is lower. A rigid transformation is first applied to the reference coordinates ($x_r$, $y_r$) to get the registered reference coordinates ($x'_r$, $y'_r$) on a 1~mm regular grid that optimally matches the evaluated distribution to the reference distribution. For each point ($x_e$, $y_e$) on $sSAR_{en}$, all points ($x'_r$, $y'_r$) are searched to identify the corresponding point with the minimal Gamma index. The validation is considered successful if $\Gamma (x_e,y_e)~\leq~1.0$ for all points. + +% \bibliographystyle{ieeetr} +% \bibliography{sample} + +% \FloatBarrier +% \clearpage +% \newpage + +% \begin{table}[htpb] \centering +% \begin{tabular}{cc|cc} +% \multicolumn{2}{c|}{\textbf{Output}} & \multicolumn{2}{c}{\textbf{Gamma Parameters}} \\ +% \textbf{Result} & \textbf{Pass Rate (\%)} & \boldmath{$\Delta D$} & \boldmath{$\Delta d$} \\\hline +% \passfail & \passrate & \deltadose & \deltadist \\ +% \end{tabular} +% \label{tab:meas-params} +% \end{table} diff --git a/src/sar_pattern_validation/report.py b/src/sar_pattern_validation/report.py index 7fb28de..566e762 100644 --- a/src/sar_pattern_validation/report.py +++ b/src/sar_pattern_validation/report.py @@ -38,6 +38,25 @@ _DOCUMENT_END_MARKER = r"\end{document}" +def _auto_measurement_area_mm(measured_csv_path: str | Path) -> tuple[float, float]: + """ + Return (x_extent_mm, y_extent_mm) of the measured CSV's coordinate grid. + + Used as the report fallback when no explicit measurement area was supplied: + the workflow processes the full extent of the measured grid in that case, + so we mirror that here. ``SARImageLoader._read_csv`` normalises x/y to + meters regardless of the original header units. + """ + # Local import to avoid pulling SimpleITK at module import time for callers + # that only need the public report API surface. + from sar_pattern_validation.image_loader import SARImageLoader + + df = SARImageLoader._read_csv(str(measured_csv_path)) + x_mm = float((df["x_m"].max() - df["x_m"].min()) * 1000.0) + y_mm = float((df["y_m"].max() - df["y_m"].min()) * 1000.0) + return x_mm, y_mm + + def _set_latex_macro(text: str, name: str, value: str) -> str: """ Substitute the body of a LaTeX macro definition. Handles both @@ -209,16 +228,30 @@ def _render_test_case_body( Path(workflow_config.measured_file_path).name ) noise_level = f"{workflow_config.noise_floor:g}" - measurement_area_x = ( - f"{workflow_config.measurement_area_x_mm:g}" - if workflow_config.measurement_area_x_mm is not None - else "---" - ) - measurement_area_y = ( - f"{workflow_config.measurement_area_y_mm:g}" - if workflow_config.measurement_area_y_mm is not None - else "---" - ) + # Resolve the measurement area written to the report. If the GUI/CLI + # supplied explicit dimensions, use them. Otherwise fall back to the + # auto-detected extent of the measured CSV (max-min of x/y, in mm) so + # the report reflects the actual area the workflow processed instead of + # showing dashes. + area_x_mm = workflow_config.measurement_area_x_mm + area_y_mm = workflow_config.measurement_area_y_mm + if area_x_mm is None or area_y_mm is None: + try: + auto_x_mm, auto_y_mm = _auto_measurement_area_mm( + workflow_config.measured_file_path + ) + if area_x_mm is None: + area_x_mm = auto_x_mm + if area_y_mm is None: + area_y_mm = auto_y_mm + except Exception as exc: # pragma: no cover - defensive fallback + LOGGER.warning( + "Could not auto-derive measurement area from %s: %s", + workflow_config.measured_file_path, + exc, + ) + measurement_area_x = f"{area_x_mm:g}" if area_x_mm is not None else "---" + measurement_area_y = f"{area_y_mm:g}" if area_y_mm is not None else "---" pssar_measured = f"{workflow_result.measured_peak_wkg:.2f}" pssar_ref = f"{workflow_result.reference_pssar:.2f}" pssar_meas = f"{workflow_result.measured_pssar:.2f}" @@ -242,18 +275,29 @@ def _render_test_case_body( r"compared to the reference, " ) - # Scaling error statement + # Scaling error statement. + # u_mr (antenna measurement uncertainty, in %) and pssar_criteria + # (combined tolerance, in %) are computed here using the same formulas + # as the Voila GUI (see voila.ipynb::_update_analytical_results). err_scale_abs = abs(100.0 * workflow_result.scaling_error) + u_mr = 14.3 if antenna_type.upper() == "VPIFAS" else 9.4 + dose_da = float(workflow_result.dose_to_agreement) + pssar_criteria = ((30.0 - dose_da) ** 2 + u_mr**2) ** 0.5 + pssar_criteria_str = f"{pssar_criteria:.1f}" + u_mr_str = f"{u_mr:g}" err_scale_tolerance = "25.0" - if err_scale_abs > float(err_scale_tolerance): + scale_statement_post = ( + rf"the tolerance of $\pm~\sqrt{{{err_scale_tolerance}~\%^2 + U_{{r,m}}^2}} " + rf"= {pssar_criteria_str}~\%$, where $U_{{r,m}}^2$ = {u_mr_str}~\% " + rf"for this antenna" + ) + if err_scale_abs > pssar_criteria: scale_statement = ( - rf"The scaling error for the psSAR is outside the " - rf"$\pm$~{err_scale_tolerance}~\% criteria." + rf"The scaling error for the psSAR is outside {scale_statement_post}." ) else: scale_statement = ( - rf"The scaling error for the psSAR is within the " - rf"$\pm$~{err_scale_tolerance}~\% criteria." + rf"The scaling error for the psSAR is within {scale_statement_post}." ) # Build the LaTeX snippet for this test case