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
58 changes: 48 additions & 10 deletions report_template/main.tex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)}
Expand All @@ -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}
Expand Down Expand Up @@ -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
Expand All @@ -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}
76 changes: 60 additions & 16 deletions src/sar_pattern_validation/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}"
Expand All @@ -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
Expand Down