{"id":6660,"date":"2026-05-12T21:15:33","date_gmt":"2026-05-13T02:15:33","guid":{"rendered":"https:\/\/ykim.synology.me\/wordpress\/?p=6660"},"modified":"2026-05-16T20:45:44","modified_gmt":"2026-05-17T01:45:44","slug":"wafer-level-zernike-polynomials","status":"publish","type":"post","link":"https:\/\/ykim.synology.me\/wordpress\/wafer-level-zernike-polynomials-6660\/","title":{"rendered":"Wafer Level Zernike Polynomials"},"content":{"rendered":"\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"800\" src=\"https:\/\/ykim.synology.me\/wordpress\/wp-content\/uploads\/2026\/05\/zernike_pyramid-800px.png\" alt=\"\" class=\"wp-image-6668\" style=\"width:600px\" srcset=\"https:\/\/ykim.synology.me\/wordpress\/wp-content\/uploads\/2026\/05\/zernike_pyramid-800px.png 800w, https:\/\/ykim.synology.me\/wordpress\/wp-content\/uploads\/2026\/05\/zernike_pyramid-800px-300x300.png 300w, https:\/\/ykim.synology.me\/wordpress\/wp-content\/uploads\/2026\/05\/zernike_pyramid-800px-150x150.png 150w, https:\/\/ykim.synology.me\/wordpress\/wp-content\/uploads\/2026\/05\/zernike_pyramid-800px-768x768.png 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/figure>\n\n\n\n<ul class=\"wp-block-social-links has-visible-labels is-layout-flex wp-block-social-links-is-layout-flex\"><li class=\"wp-social-link wp-social-link-github wp-block-social-link\"><a href=\"https:\/\/github.com\/ykim2718\/WaferLevelZernikePolynomials#stage-2--wlzpolydecompose\" class=\"wp-block-social-link-anchor\" target=\"_blank\" rel=\"noopener\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M12,2C6.477,2,2,6.477,2,12c0,4.419,2.865,8.166,6.839,9.489c0.5,0.09,0.682-0.218,0.682-0.484 c0-0.236-0.009-0.866-0.014-1.699c-2.782,0.602-3.369-1.34-3.369-1.34c-0.455-1.157-1.11-1.465-1.11-1.465 c-0.909-0.62,0.069-0.608,0.069-0.608c1.004,0.071,1.532,1.03,1.532,1.03c0.891,1.529,2.341,1.089,2.91,0.833 c0.091-0.647,0.349-1.086,0.635-1.337c-2.22-0.251-4.555-1.111-4.555-4.943c0-1.091,0.39-1.984,1.03-2.682 C6.546,8.54,6.202,7.524,6.746,6.148c0,0,0.84-0.269,2.75,1.025C10.295,6.95,11.15,6.84,12,6.836 c0.85,0.004,1.705,0.114,2.504,0.336c1.909-1.294,2.748-1.025,2.748-1.025c0.546,1.376,0.202,2.394,0.1,2.646 c0.64,0.699,1.026,1.591,1.026,2.682c0,3.841-2.337,4.687-4.565,4.935c0.359,0.307,0.679,0.917,0.679,1.852 c0,1.335-0.012,2.415-0.012,2.741c0,0.269,0.18,0.579,0.688,0.481C19.138,20.161,22,16.416,22,12C22,6.477,17.523,2,12,2z\"><\/path><\/svg><span class=\"wp-block-social-link-label\">GitHub<\/span><\/a><\/li><\/ul>\n\n\n<p><div class=\"github-readme-container markdown-body\"><div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">wlzpoly \u2014 Wafer-Level Zernike Polynomials<\/h1><a id=\"user-content-wlzpoly--wafer-level-zernike-polynomials\" class=\"anchor\" aria-label=\"Permalink: wlzpoly \u2014 Wafer-Level Zernike Polynomials\" href=\"#wlzpoly--wafer-level-zernike-polynomials\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Decompose 13-point wafer thickness measurements into 9 Zernike coefficients (LSQ \/ Ridge), with a reproducible demo workflow that generates synthetic data, fits it, and verifies the recovered coefficients against ground truth.<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Install<\/h2><a id=\"user-content-install\" class=\"anchor\" aria-label=\"Permalink: Install\" href=\"#install\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pip install wlzpoly\"><pre>pip install wlzpoly<\/pre><\/div>\n<p dir=\"auto\">Requires Python 3.9+. Dependencies: <code>numpy<\/code>, <code>pandas<\/code>, <code>matplotlib<\/code>, <code>tqdm<\/code>.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Public API<\/h2><a id=\"user-content-public-api\" class=\"anchor\" aria-label=\"Permalink: Public API\" href=\"#public-api\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"from wlzpoly import (\n    ZernikePolynomials,           # pure-math + per-wavefront instance\n    WaferLevelZernikePolynomials, # wafer-aware (coords + measurements -&gt; fit)\n    fit_lsq, fit_ridge,           # general-purpose linear solvers\n)\"><pre><span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">wlzpoly<\/span> <span class=\"pl-k\">import<\/span> (\n    <span class=\"pl-v\">ZernikePolynomials<\/span>,           <span class=\"pl-c\"># pure-math + per-wavefront instance<\/span>\n    <span class=\"pl-v\">WaferLevelZernikePolynomials<\/span>, <span class=\"pl-c\"># wafer-aware (coords + measurements -&gt; fit)<\/span>\n    <span class=\"pl-s1\">fit_lsq<\/span>, <span class=\"pl-s1\">fit_ridge<\/span>,           <span class=\"pl-c\"># general-purpose linear solvers<\/span>\n)<\/pre><\/div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Function \/ class<\/th>\n<th>Purpose<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>ZernikePolynomials.basis(j, rho, theta)<\/code><\/td>\n<td>Single Zernike basis Z_j(\u03c1, \u03b8)<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.basis_matrix(rho, theta, n_terms=\u2026)<\/code><\/td>\n<td>Design matrix A for fitting<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.pyramid_image(n_max=\u2026, names=\u2026, return_type=\u2026)<\/code><\/td>\n<td>Zernike pyramid PNG \/ Figure<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials(coeffs=\u2026).evaluate(rho, theta)<\/code><\/td>\n<td>Evaluate a specific wavefront<\/td>\n<\/tr>\n<tr>\n<td><code>WaferLevelZernikePolynomials(coords_df, coordinate, n_terms)<\/code><\/td>\n<td>Pre-compute A from measurement layout<\/td>\n<\/tr>\n<tr>\n<td><code>wlz.fit_coefficients(mesured_df, solver, lam)<\/code><\/td>\n<td>Per-wafer LSQ \/ Ridge fit<\/td>\n<\/tr>\n<tr>\n<td><code>wlz.draw_field(coeffs)<\/code><\/td>\n<td>Heatmap with measurement-point overlay<\/td>\n<\/tr>\n<tr>\n<td><code>fit_lsq(A, T)<\/code><\/td>\n<td>\u00e2 = (A\u1d40A)\u207b\u00b9 A\u1d40T<\/td>\n<\/tr>\n<tr>\n<td><code>fit_ridge(A, T, lam)<\/code><\/td>\n<td>\u00e2 = (A\u1d40A + \u03bbI)\u207b\u00b9 A\u1d40T<\/td>\n<\/tr>\n<tr>\n<td><code>loocv_lambda(A, T, lambdas)<\/code><\/td>\n<td>LOOCV-driven \u03bb selection<\/td>\n<\/tr>\n<tr>\n<td><code>wlzpoly.decompose.load_wafer_coordinates(wafer_points_file, coordinate)<\/code><\/td>\n<td>Read points JSON into a DataFrame<\/td>\n<\/tr>\n<tr>\n<td><code>wlzpoly.decompose.load_measured_data(target_file)<\/code><\/td>\n<td>Read target CSV into long-format DataFrame<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">See <a href=\"#module-reference\">Module reference<\/a> below for the design rationale of each mode.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick start<\/h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick start\" href=\"#quick-start\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import numpy as np\nfrom wlzpoly import ZernikePolynomials, WaferLevelZernikePolynomials\n\n# 1) Build a wavefront from known coefficients (Noll j \u2192 a_j)\nz = ZernikePolynomials(coeffs={1: 500.0, 4: -12.0, 6: 0.5}, n_terms=9)\nfield = z.evaluate(rho=np.array([0.0, 0.5, 1.0]), theta=np.array([0.0, 0.0, 0.0]))\n\n# 2) Fit Zernike coefficients from measurements at known coordinates\n#    coords_df : DataFrame indexed by point_id, columns ['x','y'] (mm),\n#                attrs['wafer_radius_mm']\n#    df_measured : DataFrame indexed by MultiIndex(wafer_id, point_id), column ['T']\nwlz = WaferLevelZernikePolynomials(\n    coords_df=coords_df, coordinate=&quot;cartesian&quot;, n_terms=9,\n)\nfit_results = wlz.fit_coefficients(mesured_df=df_measured, solver=&quot;lsq&quot;)\n# fit_results : list of {&quot;id&quot;: &lt;wafer_id&gt;, &quot;coeffs&quot;: np.ndarray}\n\n# 3) Render a fitted wafer field\nfig = wlz.draw_field(coeffs=fit_results[0][&quot;coeffs&quot;])\nfig.savefig(&quot;W_01_fit.png&quot;, dpi=130, bbox_inches=&quot;tight&quot;)\"><pre><span class=\"pl-k\">import<\/span> <span class=\"pl-s1\">numpy<\/span> <span class=\"pl-k\">as<\/span> <span class=\"pl-s1\">np<\/span>\n<span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">wlzpoly<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">ZernikePolynomials<\/span>, <span class=\"pl-v\">WaferLevelZernikePolynomials<\/span>\n\n<span class=\"pl-c\"># 1) Build a wavefront from known coefficients (Noll j \u2192 a_j)<\/span>\n<span class=\"pl-s1\">z<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">ZernikePolynomials<\/span>(<span class=\"pl-s1\">coeffs<\/span><span class=\"pl-c1\">=<\/span>{<span class=\"pl-c1\">1<\/span>: <span class=\"pl-c1\">500.0<\/span>, <span class=\"pl-c1\">4<\/span>: <span class=\"pl-c1\">-<\/span><span class=\"pl-c1\">12.0<\/span>, <span class=\"pl-c1\">6<\/span>: <span class=\"pl-c1\">0.5<\/span>}, <span class=\"pl-s1\">n_terms<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">9<\/span>)\n<span class=\"pl-s1\">field<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">z<\/span>.<span class=\"pl-c1\">evaluate<\/span>(<span class=\"pl-s1\">rho<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">np<\/span>.<span class=\"pl-c1\">array<\/span>([<span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-c1\">0.5<\/span>, <span class=\"pl-c1\">1.0<\/span>]), <span class=\"pl-s1\">theta<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">np<\/span>.<span class=\"pl-c1\">array<\/span>([<span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-c1\">0.0<\/span>]))\n\n<span class=\"pl-c\"># 2) Fit Zernike coefficients from measurements at known coordinates<\/span>\n<span class=\"pl-c\">#    coords_df : DataFrame indexed by point_id, columns ['x','y'] (mm),<\/span>\n<span class=\"pl-c\">#                attrs['wafer_radius_mm']<\/span>\n<span class=\"pl-c\">#    df_measured : DataFrame indexed by MultiIndex(wafer_id, point_id), column ['T']<\/span>\n<span class=\"pl-s1\">wlz<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">WaferLevelZernikePolynomials<\/span>(\n    <span class=\"pl-s1\">coords_df<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">coords_df<\/span>, <span class=\"pl-s1\">coordinate<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"cartesian\"<\/span>, <span class=\"pl-s1\">n_terms<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">9<\/span>,\n)\n<span class=\"pl-s1\">fit_results<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">wlz<\/span>.<span class=\"pl-c1\">fit_coefficients<\/span>(<span class=\"pl-s1\">mesured_df<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">df_measured<\/span>, <span class=\"pl-s1\">solver<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"lsq\"<\/span>)\n<span class=\"pl-c\"># fit_results : list of {\"id\": &lt;wafer_id&gt;, \"coeffs\": np.ndarray}<\/span>\n\n<span class=\"pl-c\"># 3) Render a fitted wafer field<\/span>\n<span class=\"pl-s1\">fig<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">wlz<\/span>.<span class=\"pl-c1\">draw_field<\/span>(<span class=\"pl-s1\">coeffs<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">fit_results<\/span>[<span class=\"pl-c1\">0<\/span>][<span class=\"pl-s\">\"coeffs\"<\/span>])\n<span class=\"pl-s1\">fig<\/span>.<span class=\"pl-c1\">savefig<\/span>(<span class=\"pl-s\">\"W_01_fit.png\"<\/span>, <span class=\"pl-s1\">dpi<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">130<\/span>, <span class=\"pl-s1\">bbox_inches<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"tight\"<\/span>)<\/pre><\/div>\n<p dir=\"auto\"><code>ZernikePolynomials<\/code> follows the <strong>Noll convention<\/strong> and supports any radial order (j \u2192 (n, m) is computed dynamically).<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Folder layout<\/h2><a id=\"user-content-folder-layout\" class=\"anchor\" aria-label=\"Permalink: Folder layout\" href=\"#folder-layout\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"WaferLevelZernikePolynomials\/\n\u2502\n\u251c\u2500\u2500 pyproject.toml             \u2190 PyPI package metadata (name=&quot;wlzpoly&quot;)\n\u251c\u2500\u2500 LICENSE                    \u2190 MIT\n\u251c\u2500\u2500 MANIFEST.in                \u2190 sdist inclusion rules\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 upload_to_pypi.ps1         \u2190 build + twine upload\n\u251c\u2500\u2500 upload_to_github.ps1       \u2190 idempotent git add\/commit\/push helper\n\u2502\n\u251c\u2500\u2500 src\/\n\u2502   \u2514\u2500\u2500 wlzpoly\/               \u2190 installed library code\n\u2502       \u251c\u2500\u2500 __init__.py        \u2190 public API (ZernikePolynomials, fit_lsq, ...)\n\u2502       \u251c\u2500\u2500 zernike_polynomials.py  \u2190 Zernike classes (math)\n\u2502       \u251c\u2500\u2500 regression.py      \u2190 LSQ \/ Ridge \/ LOOCV solvers\n\u2502       \u251c\u2500\u2500 decompose.py       \u2190 Stage 2: fitting (recover Zernike coefficients)\n\u2502       \u251c\u2500\u2500 verify.py          \u2190 Stage 3: verification + visualization\n\u2502       \u2514\u2500\u2500 reconstruct.py     \u2190 (optional) inverse of decompose: T = A\u00b7a\n\u2502\n\u2514\u2500\u2500 examples\/                  \u2190 demo (NOT installed via pip)\n    \u251c\u2500\u2500 generate_samples.py    \u2190 Stage 1: synthetic data generation\n    \u251c\u2500\u2500 run_demo.ps1           \u2190 runs all three stages end-to-end\n    \u251c\u2500\u2500 generate_pyramid_image.py  \u2190 (optional) Zernike basis-function reference chart\n    \u251c\u2500\u2500 configuration\/         \u2190 inputs (settings + measurement layout)\n    \u2502   \u251c\u2500\u2500 config.json        \u2190 generate_samples settings (scenarios, drift)\n    \u2502   \u2514\u2500\u2500 points_13.json     \u2190 13-point measurement coordinates\n    \u251c\u2500\u2500 1_samples\/             \u2190 Stage 1 outputs (committed for browsing)\n    \u251c\u2500\u2500 2_decomposition\/       \u2190 Stage 2 outputs\n    \u251c\u2500\u2500 3_verification\/        \u2190 Stage 3 outputs\n    \u2514\u2500\u2500 4_reconstruction\/      \u2190 (optional) wlzpoly.reconstruct outputs\"><pre class=\"notranslate\"><code>WaferLevelZernikePolynomials\/\n\u2502\n\u251c\u2500\u2500 pyproject.toml             \u2190 PyPI package metadata (name=\"wlzpoly\")\n\u251c\u2500\u2500 LICENSE                    \u2190 MIT\n\u251c\u2500\u2500 MANIFEST.in                \u2190 sdist inclusion rules\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 upload_to_pypi.ps1         \u2190 build + twine upload\n\u251c\u2500\u2500 upload_to_github.ps1       \u2190 idempotent git add\/commit\/push helper\n\u2502\n\u251c\u2500\u2500 src\/\n\u2502   \u2514\u2500\u2500 wlzpoly\/               \u2190 installed library code\n\u2502       \u251c\u2500\u2500 __init__.py        \u2190 public API (ZernikePolynomials, fit_lsq, ...)\n\u2502       \u251c\u2500\u2500 zernike_polynomials.py  \u2190 Zernike classes (math)\n\u2502       \u251c\u2500\u2500 regression.py      \u2190 LSQ \/ Ridge \/ LOOCV solvers\n\u2502       \u251c\u2500\u2500 decompose.py       \u2190 Stage 2: fitting (recover Zernike coefficients)\n\u2502       \u251c\u2500\u2500 verify.py          \u2190 Stage 3: verification + visualization\n\u2502       \u2514\u2500\u2500 reconstruct.py     \u2190 (optional) inverse of decompose: T = A\u00b7a\n\u2502\n\u2514\u2500\u2500 examples\/                  \u2190 demo (NOT installed via pip)\n    \u251c\u2500\u2500 generate_samples.py    \u2190 Stage 1: synthetic data generation\n    \u251c\u2500\u2500 run_demo.ps1           \u2190 runs all three stages end-to-end\n    \u251c\u2500\u2500 generate_pyramid_image.py  \u2190 (optional) Zernike basis-function reference chart\n    \u251c\u2500\u2500 configuration\/         \u2190 inputs (settings + measurement layout)\n    \u2502   \u251c\u2500\u2500 config.json        \u2190 generate_samples settings (scenarios, drift)\n    \u2502   \u2514\u2500\u2500 points_13.json     \u2190 13-point measurement coordinates\n    \u251c\u2500\u2500 1_samples\/             \u2190 Stage 1 outputs (committed for browsing)\n    \u251c\u2500\u2500 2_decomposition\/       \u2190 Stage 2 outputs\n    \u251c\u2500\u2500 3_verification\/        \u2190 Stage 3 outputs\n    \u2514\u2500\u2500 4_reconstruction\/      \u2190 (optional) wlzpoly.reconstruct outputs\n<\/code><\/pre><\/div>\n<p dir=\"auto\">Pre-generated demo outputs are kept under <code>examples\/{1_samples, 2_decomposition, 3_verification}\/<\/code> so the figures and CSVs can be browsed directly on the GitHub page. They are excluded from the PyPI sdist via <code>MANIFEST.in<\/code> to keep the installed package lean.<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Output folder<\/th>\n<th>Files produced<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>1_samples\/<\/code><\/td>\n<td><code>points_13.json<\/code> (copy), <code>target_file.csv<\/code> (id + P1..P13), <code>ground_truth.csv<\/code> (id + scenario + a1..a9), <code>wafer_maps.png<\/code>, <code>measurement_plot.png<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>2_decomposition\/<\/code><\/td>\n<td><code>decomposed_targets_lsq.csv<\/code> (LSQ fit), <code>decomposed_targets_ridge.csv<\/code> (Ridge fit) \u2014 each is <code>id + a1..a9<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>3_verification\/<\/code><\/td>\n<td><code>decomposition_results.csv<\/code> (truth vs lsq vs ridge), <code>decomposition_summary_lsq.png<\/code>, <code>decomposition_summary_ridge.png<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>4_reconstruction\/<\/code><\/td>\n<td><code>reconstructed_targets.csv<\/code> (id + P1..PN) \u2014 Stage 2 coefficients pushed back through <code>T = A\u00b7a<\/code>, no truth comparison<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">How to run the demo<\/h2><a id=\"user-content-how-to-run-the-demo\" class=\"anchor\" aria-label=\"Permalink: How to run the demo\" href=\"#how-to-run-the-demo\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Development install (clone + editable)<\/h3><a id=\"user-content-development-install-clone--editable\" class=\"anchor\" aria-label=\"Permalink: Development install (clone + editable)\" href=\"#development-install-clone--editable\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git clone https:\/\/github.com\/ykim2718\/WaferLevelZernikePolynomials.git\ncd WaferLevelZernikePolynomials\npip install -e .\"><pre>git clone https:\/\/github.com\/ykim2718\/WaferLevelZernikePolynomials.git\n<span class=\"pl-c1\">cd<\/span> WaferLevelZernikePolynomials\npip install -e <span class=\"pl-c1\">.<\/span><\/pre><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Three-stage demo workflow<\/h3><a id=\"user-content-three-stage-demo-workflow\" class=\"anchor\" aria-label=\"Permalink: Three-stage demo workflow\" href=\"#three-stage-demo-workflow\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">The easiest path is the bundled PowerShell runner. From inside <code>examples\/<\/code>:<\/p>\n<div class=\"highlight highlight-source-powershell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\".\\run_demo.ps1\"><pre>.\\run_demo.ps1<\/pre><\/div>\n<p dir=\"auto\">This runs Stage 1 \u2192 2 \u2192 3 sequentially with the correct flags. Outputs land in <code>examples\/1_samples\/<\/code>, <code>examples\/2_decomposition\/<\/code>, and <code>examples\/3_verification\/<\/code>.<\/p>\n<p dir=\"auto\">To call each stage manually (run from inside <code>examples\/<\/code>):<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"cd examples\n\n# Stage 1: generate synthetic measurement data\npython generate_samples.py `\n    --working_folder . `\n    --config_json .\/configuration\/config.json `\n    --wafer_points .\/configuration\/points_13.json `\n    --output_folder .\/1_samples\n\n# Stage 2a: LSQ fit -&gt; decomposed_targets_lsq.csv\npython -m wlzpoly.decompose `\n    --working_folder . `\n    --wafer_points .\/1_samples\/points_13.json `\n    --input_file .\/1_samples\/target_file.csv `\n    --output_folder .\/2_decomposition `\n    --output_file decomposed_targets_lsq.csv `\n    --n_terms 9 `\n    --solver lsq\n\n# Stage 2b: Ridge fit with LOOCV -&gt; decomposed_targets_ridge.csv\npython -m wlzpoly.decompose `\n    --working_folder . `\n    --wafer_points .\/1_samples\/points_13.json `\n    --input_file .\/1_samples\/target_file.csv `\n    --output_folder .\/2_decomposition `\n    --output_file decomposed_targets_ridge.csv `\n    --n_terms 9 `\n    --solver ridge --auto_lam --loocv_ref first_wafer\n\n# Stage 3: compare precomputed coefficients vs ground truth (no fitting)\npython -m wlzpoly.verify `\n    --decomposed_lsq_file .\/2_decomposition\/decomposed_targets_lsq.csv `\n    --decomposed_ridge_file .\/2_decomposition\/decomposed_targets_ridge.csv `\n    --ground_truth_file .\/1_samples\/ground_truth.csv `\n    --n_terms 9 `\n    --output_folder .\/3_verification\"><pre><span class=\"pl-c1\">cd<\/span> examples\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 1: generate synthetic measurement data<\/span>\npython generate_samples.py <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --working_folder <span class=\"pl-c1\">.<\/span> <span class=\"pl-pds\">`<\/span><\/span>\n    --config_json .\/configuration\/config.json <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --wafer_points .\/configuration\/points_13.json <span class=\"pl-pds\">`<\/span><\/span>\n    --output_folder .\/1_samples\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 2a: LSQ fit -&gt; decomposed_targets_lsq.csv<\/span>\npython -m wlzpoly.decompose <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --working_folder <span class=\"pl-c1\">.<\/span> <span class=\"pl-pds\">`<\/span><\/span>\n    --wafer_points .\/1_samples\/points_13.json <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --input_file .\/1_samples\/target_file.csv <span class=\"pl-pds\">`<\/span><\/span>\n    --output_folder .\/2_decomposition <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --output_file decomposed_targets_lsq.csv <span class=\"pl-pds\">`<\/span><\/span>\n    --n_terms 9 <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --solver lsq<\/span>\n<span class=\"pl-s\"><\/span>\n<span class=\"pl-s\"><span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 2b: Ridge fit with LOOCV -&gt; decomposed_targets_ridge.csv<\/span><\/span>\n<span class=\"pl-s\">python -m wlzpoly.decompose <span class=\"pl-pds\">`<\/span><\/span>\n    --working_folder <span class=\"pl-c1\">.<\/span> <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --wafer_points .\/1_samples\/points_13.json <span class=\"pl-pds\">`<\/span><\/span>\n    --input_file .\/1_samples\/target_file.csv <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --output_folder .\/2_decomposition <span class=\"pl-pds\">`<\/span><\/span>\n    --output_file decomposed_targets_ridge.csv <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --n_terms 9 <span class=\"pl-pds\">`<\/span><\/span>\n    --solver ridge --auto_lam --loocv_ref first_wafer\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 3: compare precomputed coefficients vs ground truth (no fitting)<\/span>\npython -m wlzpoly.verify <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --decomposed_lsq_file .\/2_decomposition\/decomposed_targets_lsq.csv <span class=\"pl-pds\">`<\/span><\/span>\n    --decomposed_ridge_file .\/2_decomposition\/decomposed_targets_ridge.csv <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --ground_truth_file .\/1_samples\/ground_truth.csv <span class=\"pl-pds\">`<\/span><\/span>\n    --n_terms 9 <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --output_folder .\/3_verification<\/span><\/pre><\/div>\n<p dir=\"auto\">The stages must be run in order \u2014 each one consumes the previous stage&#8217;s output. <code>wlzpoly.decompose<\/code> and <code>wlzpoly.verify<\/code> no longer read <code>config.json<\/code>; every parameter is exposed as a CLI flag.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Stage 1 \u2014 <code>generate_samples.py<\/code><\/h3><a id=\"user-content-stage-1--generate_samplespy\" class=\"anchor\" aria-label=\"Permalink: Stage 1 \u2014 generate_samples.py\" href=\"#stage-1--generate_samplespy\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Generates synthetic wafer data from a config + measurement layout.<\/p>\n<p dir=\"auto\"><strong>Inputs (<code>configuration\/<\/code>)<\/strong>: <code>config.json<\/code>, <code>points_13.json<\/code><\/p>\n<p dir=\"auto\"><strong>Outputs (<code>1_samples\/<\/code>)<\/strong>:<\/p>\n<ul dir=\"auto\">\n<li><code>points_13.json<\/code> \u2014 copy of the input (consumed by later stages)<\/li>\n<li><code>target_file.csv<\/code> \u2014 id + P1..P13 (same shape as real metrology output)<\/li>\n<li><code>ground_truth.csv<\/code> \u2014 id + scenario + a1..a9 (verification answer key)<\/li>\n<li><code>wafer_maps.png<\/code> \u2014 heatmaps of all six scenarios<\/li>\n<li><code>measurement_plot.png<\/code> \u2014 13-point measurement inspection<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Procedure<\/strong>:<\/p>\n<ol dir=\"auto\">\n<li>Load per-scenario ground-truth coefficients (a\u2081..a\u2089) from config<\/li>\n<li>Evaluate <code>T_clean = \u03a3 a_k \u00b7 Z_k(\u03c1_i, \u03b8_i)<\/code> at the 13 measurement points<\/li>\n<li>Add Gaussian noise \u2192 <code>T = T_clean + \u03b5<\/code><\/li>\n<li>Save <code>T<\/code> as CSV; save the ground-truth coefficients to a separate CSV<\/li>\n<\/ol>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Stage 2 \u2014 <code>wlzpoly.decompose<\/code><\/h3><a id=\"user-content-stage-2--wlzpolydecompose\" class=\"anchor\" aria-label=\"Permalink: Stage 2 \u2014 wlzpoly.decompose\" href=\"#stage-2--wlzpolydecompose\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Recovers <code>n_terms<\/code> Zernike coefficients from the N-point measurements. Run <strong>once per solver<\/strong> \u2014 Stage 2a (LSQ) and Stage 2b (Ridge with optional LOOCV-tuned \u03bb) write separate CSVs.<\/p>\n<p dir=\"auto\"><strong>Inputs (<code>1_samples\/<\/code>)<\/strong>: <code>points_13.json<\/code>, <code>target_file.csv<\/code><\/p>\n<p dir=\"auto\"><strong>Outputs (<code>2_decomposition\/<\/code>)<\/strong>:<\/p>\n<ul dir=\"auto\">\n<li><code>decomposed_targets_lsq.csv<\/code> \u2014 id + a1..aN (LSQ fit)<\/li>\n<li><code>decomposed_targets_ridge.csv<\/code> \u2014 id + a1..aN (Ridge fit, \u03bb from <code>--lam<\/code> or LOOCV)<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>LOOCV (<code>--auto_lam<\/code>)<\/strong>: When <code>--solver ridge --auto_lam<\/code> is set, the module picks \u03bb from <code>--loocv_lambdas<\/code> automatically. The reference T used for the scan is controlled by <code>--loocv_ref<\/code>:<\/p>\n<ul dir=\"auto\">\n<li><code>first_wafer<\/code> (default) \u2014 use T of the first wafer; apply that \u03bb to all wafers<\/li>\n<li><code>mean<\/code> \u2014 use per-point mean across all wafers<\/li>\n<li><code>per_wafer<\/code> \u2014 run LOOCV per wafer (N\u00d7 slower, each wafer gets its own \u03bb)<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Provided functions<\/strong> (also re-usable from Python code):<\/p>\n<ul dir=\"auto\">\n<li><code>load_wafer_coordinates(*, wafer_points_file, coordinate)<\/code> \u2192\n<code>pd.DataFrame<\/code> (index=<code>point_id<\/code>, columns <code>x, y<\/code> or <code>r, theta<\/code>).\nWhen <code>--coordinate cartesian<\/code> only x and y are read; for <code>polar<\/code> only r and theta.\n<code>df.attrs['wafer_radius_mm']<\/code> is populated.<\/li>\n<li><code>load_measured_data(*, target_file)<\/code> \u2192\n<code>pd.DataFrame<\/code> (index=<code>MultiIndex(wafer_id, point_id)<\/code>, columns <code>['T']<\/code>).\n<strong>Coordinates are not merged in<\/strong> \u2014 measurements only.<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Procedure<\/strong>:<\/p>\n<ol dir=\"auto\">\n<li><code>load_wafer_coordinates()<\/code> \u2192 coordinate DataFrame<\/li>\n<li><code>load_measured_data()<\/code> \u2192 measurement DataFrame<\/li>\n<li><code>WaferLevelZernikePolynomials(coords_df=\u2026, coordinate=\u2026, n_terms=\u2026)<\/code> \u2014\nconstructor pre-computes the basis matrix <code>A<\/code> on <code>wlz.A<\/code><\/li>\n<li><code>wlz.fit_coefficients(df_measured=\u2026, solver=\u2026, lam=\u2026)<\/code> \u2192\nper-wafer <code>regression.fit_lsq(A, T)<\/code> \u2192 \u00e2 = (A\u1d40A)\u207b\u00b9 A\u1d40 T.\n<code>T<\/code> is reindexed to <code>coords_df.index<\/code>, matching the row order of <code>A<\/code>.<\/li>\n<li>Save the result as CSV<\/li>\n<\/ol>\n<p dir=\"auto\"><code>ground_truth.csv<\/code> is not consumed at this stage.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Stage 3 \u2014 <code>wlzpoly.verify<\/code><\/h3><a id=\"user-content-stage-3--wlzpolyverify\" class=\"anchor\" aria-label=\"Permalink: Stage 3 \u2014 wlzpoly.verify\" href=\"#stage-3--wlzpolyverify\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Compares Stage 2&#8217;s precomputed coefficients against ground truth. <strong>No fitting happens here<\/strong> \u2014 both decomposed CSVs are read directly.<\/p>\n<p dir=\"auto\"><strong>Inputs<\/strong>:<\/p>\n<ul dir=\"auto\">\n<li><code>2_decomposition\/decomposed_targets_lsq.csv<\/code> (optional; skip if missing)<\/li>\n<li><code>2_decomposition\/decomposed_targets_ridge.csv<\/code> (optional; skip if missing)<\/li>\n<li><code>1_samples\/ground_truth.csv<\/code><\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Outputs (<code>3_verification\/<\/code>)<\/strong>:<\/p>\n<ul dir=\"auto\">\n<li><code>decomposition_results.csv<\/code> \u2014 id + scenario + truth\/lsq\/ridge \u00d7 n_terms (columns conditional on which solvers given)<\/li>\n<li><code>decomposition_summary_lsq.png<\/code> \u2014 truth vs LSQ bar chart (only if LSQ given)<\/li>\n<li><code>decomposition_summary_ridge.png<\/code> \u2014 truth vs Ridge bar chart (only if Ridge given)<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Procedure<\/strong>:<\/p>\n<ol dir=\"auto\">\n<li>Read decomposed coefficient CSVs (LSQ and\/or Ridge) and ground_truth<\/li>\n<li>Join by wafer id \u2192 <code>[{id, scenario, truth, lsq?, ridge?}, ...]<\/code><\/li>\n<li>Print per-scenario comparison table to the console<\/li>\n<li>Print per-coefficient RMSE summary<\/li>\n<li>Render scenario-level bar charts as PNG<\/li>\n<\/ol>\n<p dir=\"auto\">At least one of the two decomposed files must exist. If only one is given, that solver&#8217;s chart is the only one produced.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Optional \u2014 Zernike pyramid reference chart<\/h3><a id=\"user-content-optional--zernike-pyramid-reference-chart\" class=\"anchor\" aria-label=\"Permalink: Optional \u2014 Zernike pyramid reference chart\" href=\"#optional--zernike-pyramid-reference-chart\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\"><code>generate_pyramid_image.py<\/code> renders the canonical Noll pyramid (basis functions themselves, not any wafer data). Independent of the three-stage demo. Run it when you need a fresh reference image for docs or slides.<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python generate_pyramid_image.py --with_names\"><pre>python generate_pyramid_image.py --with_names<\/pre><\/div>\n<p dir=\"auto\">Outputs <code>zernike_pyramid.png<\/code> in the script&#8217;s folder by default. Key CLI options: <code>--n_max<\/code> (default 4 \u2192 15 terms), <code>--with_names<\/code> (Piston\/Tilt X\/&#8230; labels), <code>--output_folder<\/code>, <code>--output_file<\/code>, <code>--cmap<\/code>. See <code>-h<\/code> for the full list.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Stage 4 \u2014 <code>wlzpoly.reconstruct<\/code><\/h3><a id=\"user-content-stage-4--wlzpolyreconstruct\" class=\"anchor\" aria-label=\"Permalink: Stage 4 \u2014 wlzpoly.reconstruct\" href=\"#stage-4--wlzpolyreconstruct\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\"><code>wlzpoly.reconstruct<\/code> is the <strong>inverse of <code>decompose<\/code><\/strong>: it pushes the fitted Zernike coefficients back through the basis matrix to recover the N-point measurement profile (<code>T = A\u00b7a<\/code>). No ground-truth comparison and no R\u00b2 \u2014 intended for production \/ inference use where the true T is unknown.<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python -m wlzpoly.reconstruct `\n    --input_folder . `\n    --wafer_point_json .\/1_samples\/points_13.json `\n    --decomposed_file .\/2_decomposition\/decomposed_targets_lsq.csv `\n    --output_folder .\/4_reconstruction `\n    --output_file reconstructed_lsq.csv `\n    --n_terms 9 `\n    --col_wafer_id id\"><pre>python -m wlzpoly.reconstruct <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --input_folder <span class=\"pl-c1\">.<\/span> <span class=\"pl-pds\">`<\/span><\/span>\n    --wafer_point_json .\/1_samples\/points_13.json <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --decomposed_file .\/2_decomposition\/decomposed_targets_lsq.csv <span class=\"pl-pds\">`<\/span><\/span>\n    --output_folder .\/4_reconstruction <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --output_file reconstructed_lsq.csv <span class=\"pl-pds\">`<\/span><\/span>\n    --n_terms 9 <span class=\"pl-s\"><span class=\"pl-pds\">`<\/span><\/span>\n<span class=\"pl-s\">    --col_wafer_id id<\/span><\/pre><\/div>\n<p dir=\"auto\">Output: <code>4_reconstruction\/reconstructed_lsq.csv<\/code> (<code>id + P1..PN<\/code>, same wide shape as Stage 1&#8217;s <code>target_file.csv<\/code>). The <code>reconstruct()<\/code> Python API returns a <code>pd.DataFrame<\/code> only \u2014 the CLI handles CSV writing.<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Data flow<\/h2><a id=\"user-content-data-flow\" class=\"anchor\" aria-label=\"Permalink: Data flow\" href=\"#data-flow\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502  configuration\/        \u2502\n                  \u2502   config.json          \u2502\n                  \u2502   points_13.json       \u2502\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n                               \u25bc\n                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502 generate_samples.py    \u2502  Stage 1\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n                               \u25bc\n                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502  1_samples\/            \u2502  target_file.csv +\n                  \u2502                        \u2502  ground_truth.csv +\n                  \u2502                        \u2502  points_13.json (copy)\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                            \u2502  target_file + wafer_points\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502                            \u2502\n              \u25bc                            \u25bc\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n    \u2502 decompose.py     \u2502 Stage 2 \u2502 decompose.py     \u2502 Stage 2\n    \u2502  --solver lsq    \u2502         \u2502  --solver ridge  \u2502\n    \u2502                  \u2502         \u2502   --auto_lam     \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n             \u2502                            \u2502\n             \u25bc                            \u25bc\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n    \u2502  2_decomposition\/                           \u2502\n    \u2502   decomposed_targets_lsq.csv                \u2502\n    \u2502   decomposed_targets_ridge.csv              \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502                          \u2502\n               \u2502 + ground_truth.csv       \u2502 + wafer_point_json\n               \u2502   (from 1_samples)       \u2502   (basis A)\n               \u25bc                          \u25bc\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510           \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502  verify.py   \u2502  Stage 3  \u2502 reconstruct.py   \u2502  Stage 4\n       \u2502 (no fitting) \u2502           \u2502 (T_recon = A\u00b7a)  \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518           \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n              \u2502                            \u2502\n              \u25bc                            \u25bc\n      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n      \u2502 3_verification\/  \u2502         \u2502 4_reconstruction\/\u2502\n      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\"><pre class=\"notranslate\"><code>                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502  configuration\/        \u2502\n                  \u2502   config.json          \u2502\n                  \u2502   points_13.json       \u2502\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n                               \u25bc\n                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502 generate_samples.py    \u2502  Stage 1\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n                               \u25bc\n                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502  1_samples\/            \u2502  target_file.csv +\n                  \u2502                        \u2502  ground_truth.csv +\n                  \u2502                        \u2502  points_13.json (copy)\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                            \u2502  target_file + wafer_points\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502                            \u2502\n              \u25bc                            \u25bc\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n    \u2502 decompose.py     \u2502 Stage 2 \u2502 decompose.py     \u2502 Stage 2\n    \u2502  --solver lsq    \u2502         \u2502  --solver ridge  \u2502\n    \u2502                  \u2502         \u2502   --auto_lam     \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n             \u2502                            \u2502\n             \u25bc                            \u25bc\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n    \u2502  2_decomposition\/                           \u2502\n    \u2502   decomposed_targets_lsq.csv                \u2502\n    \u2502   decomposed_targets_ridge.csv              \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502                          \u2502\n               \u2502 + ground_truth.csv       \u2502 + wafer_point_json\n               \u2502   (from 1_samples)       \u2502   (basis A)\n               \u25bc                          \u25bc\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510           \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502  verify.py   \u2502  Stage 3  \u2502 reconstruct.py   \u2502  Stage 4\n       \u2502 (no fitting) \u2502           \u2502 (T_recon = A\u00b7a)  \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518           \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n              \u2502                            \u2502\n              \u25bc                            \u25bc\n      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n      \u2502 3_verification\/  \u2502         \u2502 4_reconstruction\/\u2502\n      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre><\/div>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Module reference<\/h2><a id=\"user-content-module-reference\" class=\"anchor\" aria-label=\"Permalink: Module reference\" href=\"#module-reference\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>zernike_polynomials.py<\/code><\/h3><a id=\"user-content-zernike_polynomialspy\" class=\"anchor\" aria-label=\"Permalink: zernike_polynomials.py\" href=\"#zernike_polynomialspy\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Zernike polynomial library. Follows the Noll convention; the j \u2192 (n, m) mapping is computed dynamically by the standard algorithm, so any radial order is supported.<\/p>\n<p dir=\"auto\"><code>ZernikePolynomials<\/code> exposes <strong>three usage modes<\/strong>:<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Mode 1: pure math (class-level)<\/h4><a id=\"user-content-mode-1-pure-math-class-level\" class=\"anchor\" aria-label=\"Permalink: Mode 1: pure math (class-level)\" href=\"#mode-1-pure-math-class-level\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Operations independent of any specific coefficient set. Call as <code>ZernikePolynomials.method()<\/code>.<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Purpose<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>ZernikePolynomials.nm_from_noll(j)<\/code><\/td>\n<td>Noll index j \u2192 (n, m)<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.radial(n, m, rho)<\/code><\/td>\n<td>Radial polynomial R_n^m(\u03c1)<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.basis(j, rho, theta)<\/code><\/td>\n<td>Z_j(\u03c1, \u03b8)<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.basis_matrix(rho, theta, n_terms=\u2026)<\/code><\/td>\n<td>Design matrix A for fitting<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.to_polar(x=\u2026, y=\u2026)<\/code><\/td>\n<td>Cartesian \u2192 polar (r, theta_rad)<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.to_cartesian(r=\u2026, theta=\u2026)<\/code><\/td>\n<td>Polar \u2192 Cartesian (x, y)<\/td>\n<\/tr>\n<tr>\n<td><code>ZernikePolynomials.pyramid_image(n_max=\u2026, names=\u2026, return_type='png'|'figure')<\/code> \u2192 <code>Union[bytes, Figure]<\/code><\/td>\n<td>Zernike pyramid image (PNG bytes or Figure)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Mode 2: specific wavefront (instance)<\/h4><a id=\"user-content-mode-2-specific-wavefront-instance\" class=\"anchor\" aria-label=\"Permalink: Mode 2: specific wavefront (instance)\" href=\"#mode-2-specific-wavefront-instance\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">An instance carrying a coefficient set \u2014 i.e. &#8220;this particular wafer\/wavefront expressed as a Zernike expansion&#8221;.<\/p>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"z = ZernikePolynomials(coeffs={1: 500.0, 4: -12.0, 6: 0.5}, n_terms=9)\n# Or\nz = ZernikePolynomials(coeffs=[500.0, 0.5, -0.3, -2.0, 0.1, 0.2, 0, 0, 0])  # array OK\n\n# Evaluation\nfield = z.evaluate(rho=rho, theta=theta)  # ndarray\n\n# Indexing \/ metadata\nz[4]                # -12.0 (a_4)\nz[99]               # 0.0 (undefined j returns 0)\nlen(z)              # 9 (n_terms)\nrepr(z)             # &quot;ZernikePolynomials(n_terms=9, a1=500.000, dominant=a4=-12.000)&quot;\nz.rms()             # sqrt(sum a_j^2), piston excluded by default\nz.rms(exclude_piston=False)\"><pre><span class=\"pl-s1\">z<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">ZernikePolynomials<\/span>(<span class=\"pl-s1\">coeffs<\/span><span class=\"pl-c1\">=<\/span>{<span class=\"pl-c1\">1<\/span>: <span class=\"pl-c1\">500.0<\/span>, <span class=\"pl-c1\">4<\/span>: <span class=\"pl-c1\">-<\/span><span class=\"pl-c1\">12.0<\/span>, <span class=\"pl-c1\">6<\/span>: <span class=\"pl-c1\">0.5<\/span>}, <span class=\"pl-s1\">n_terms<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">9<\/span>)\n<span class=\"pl-c\"># Or<\/span>\n<span class=\"pl-s1\">z<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">ZernikePolynomials<\/span>(<span class=\"pl-s1\">coeffs<\/span><span class=\"pl-c1\">=<\/span>[<span class=\"pl-c1\">500.0<\/span>, <span class=\"pl-c1\">0.5<\/span>, <span class=\"pl-c1\">-<\/span><span class=\"pl-c1\">0.3<\/span>, <span class=\"pl-c1\">-<\/span><span class=\"pl-c1\">2.0<\/span>, <span class=\"pl-c1\">0.1<\/span>, <span class=\"pl-c1\">0.2<\/span>, <span class=\"pl-c1\">0<\/span>, <span class=\"pl-c1\">0<\/span>, <span class=\"pl-c1\">0<\/span>])  <span class=\"pl-c\"># array OK<\/span>\n\n<span class=\"pl-c\"># Evaluation<\/span>\n<span class=\"pl-s1\">field<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">z<\/span>.<span class=\"pl-c1\">evaluate<\/span>(<span class=\"pl-s1\">rho<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">rho<\/span>, <span class=\"pl-s1\">theta<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">theta<\/span>)  <span class=\"pl-c\"># ndarray<\/span>\n\n<span class=\"pl-c\"># Indexing \/ metadata<\/span>\n<span class=\"pl-s1\">z<\/span>[<span class=\"pl-c1\">4<\/span>]                <span class=\"pl-c\"># -12.0 (a_4)<\/span>\n<span class=\"pl-s1\">z<\/span>[<span class=\"pl-c1\">99<\/span>]               <span class=\"pl-c\"># 0.0 (undefined j returns 0)<\/span>\n<span class=\"pl-en\">len<\/span>(<span class=\"pl-s1\">z<\/span>)              <span class=\"pl-c\"># 9 (n_terms)<\/span>\n<span class=\"pl-en\">repr<\/span>(<span class=\"pl-s1\">z<\/span>)             <span class=\"pl-c\"># \"ZernikePolynomials(n_terms=9, a1=500.000, dominant=a4=-12.000)\"<\/span>\n<span class=\"pl-s1\">z<\/span>.<span class=\"pl-c1\">rms<\/span>()             <span class=\"pl-c\"># sqrt(sum a_j^2), piston excluded by default<\/span>\n<span class=\"pl-s1\">z<\/span>.<span class=\"pl-c1\">rms<\/span>(<span class=\"pl-s1\">exclude_piston<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">False<\/span>)<\/pre><\/div>\n<p dir=\"auto\">For a wafer heatmap (with measurement-point overlay) use <code>WaferLevelZernikePolynomials.draw_field(coeffs=\u2026)<\/code>.<\/p>\n<p dir=\"auto\"><strong>Why split it out<\/strong>: <code>basis()<\/code> and <code>nm_from_noll()<\/code> are pure math that doesn&#8217;t need a coefficient set, so they live at class level. <code>evaluate()<\/code> and <code>rms()<\/code> require a specific coefficient set and are therefore instance methods. Visualization (<code>draw_field<\/code>) only makes sense once measurement-point coordinates are known, so it lives on <code>WaferLevelZernikePolynomials<\/code>.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Mode 3: specific wafer measurement layout (subclass <code>WaferLevelZernikePolynomials<\/code>)<\/h4><a id=\"user-content-mode-3-specific-wafer-measurement-layout-subclass-waferlevelzernikepolynomials\" class=\"anchor\" aria-label=\"Permalink: Mode 3: specific wafer measurement layout (subclass WaferLevelZernikePolynomials)\" href=\"#mode-3-specific-wafer-measurement-layout-subclass-waferlevelzernikepolynomials\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">A <strong>wafer-aware<\/strong> subclass of <code>ZernikePolynomials<\/code>. It accepts a measurement-point coordinate frame (<code>coords_df<\/code>), pre-computes the basis matrix <code>A<\/code> once, then fits per-wafer coefficients from a measurement DataFrame (<code>df_measured<\/code>).<\/p>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"from wlzpoly.decompose import load_wafer_coordinates, load_measured_data\nfrom wlzpoly import WaferLevelZernikePolynomials\n\ncoords_df = load_wafer_coordinates(\n    wafer_points_file=&quot;points_13.json&quot;, coordinate=&quot;cartesian&quot;,\n)\ndf_measured = load_measured_data(target_file=&quot;1_samples\/target_file.csv&quot;)\n\nwlz = WaferLevelZernikePolynomials(\n    coords_df=coords_df,\n    coordinate=&quot;cartesian&quot;,\n    n_terms=9,\n)\n# wlz.A : np.ndarray (m \u00d7 n_terms) \u2014 pre-computed basis\nfit_results = wlz.fit_coefficients(\n    mesured_df=df_measured, solver=&quot;lsq&quot;,\n)\n# fit_results : list of {&quot;id&quot;: &lt;wafer_id&gt;, &quot;coeffs&quot;: np.ndarray}\n\n# Render one wafer's fitted wavefront (heatmap + measurement points)\nfig = wlz.draw_field(coeffs=fit_results[0][&quot;coeffs&quot;])\nfig.savefig(&quot;W_01_fit.png&quot;, dpi=130, bbox_inches=&quot;tight&quot;)\"><pre><span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">wlzpoly<\/span>.<span class=\"pl-s1\">decompose<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-s1\">load_wafer_coordinates<\/span>, <span class=\"pl-s1\">load_measured_data<\/span>\n<span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">wlzpoly<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">WaferLevelZernikePolynomials<\/span>\n\n<span class=\"pl-s1\">coords_df<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">load_wafer_coordinates<\/span>(\n    <span class=\"pl-s1\">wafer_points_file<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"points_13.json\"<\/span>, <span class=\"pl-s1\">coordinate<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"cartesian\"<\/span>,\n)\n<span class=\"pl-s1\">df_measured<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">load_measured_data<\/span>(<span class=\"pl-s1\">target_file<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"1_samples\/target_file.csv\"<\/span>)\n\n<span class=\"pl-s1\">wlz<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">WaferLevelZernikePolynomials<\/span>(\n    <span class=\"pl-s1\">coords_df<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">coords_df<\/span>,\n    <span class=\"pl-s1\">coordinate<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"cartesian\"<\/span>,\n    <span class=\"pl-s1\">n_terms<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">9<\/span>,\n)\n<span class=\"pl-c\"># wlz.A : np.ndarray (m \u00d7 n_terms) \u2014 pre-computed basis<\/span>\n<span class=\"pl-s1\">fit_results<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">wlz<\/span>.<span class=\"pl-c1\">fit_coefficients<\/span>(\n    <span class=\"pl-s1\">mesured_df<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">df_measured<\/span>, <span class=\"pl-s1\">solver<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"lsq\"<\/span>,\n)\n<span class=\"pl-c\"># fit_results : list of {\"id\": &lt;wafer_id&gt;, \"coeffs\": np.ndarray}<\/span>\n\n<span class=\"pl-c\"># Render one wafer's fitted wavefront (heatmap + measurement points)<\/span>\n<span class=\"pl-s1\">fig<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">wlz<\/span>.<span class=\"pl-c1\">draw_field<\/span>(<span class=\"pl-s1\">coeffs<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">fit_results<\/span>[<span class=\"pl-c1\">0<\/span>][<span class=\"pl-s\">\"coeffs\"<\/span>])\n<span class=\"pl-s1\">fig<\/span>.<span class=\"pl-c1\">savefig<\/span>(<span class=\"pl-s\">\"W_01_fit.png\"<\/span>, <span class=\"pl-s1\">dpi<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">130<\/span>, <span class=\"pl-s1\">bbox_inches<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"tight\"<\/span>)<\/pre><\/div>\n<p dir=\"auto\"><strong>Why split it out<\/strong>: the base <code>ZernikePolynomials<\/code> covers &#8220;coefficients are already known&#8221; scenarios (sample generation, ground truth), while <code>WaferLevelZernikePolynomials<\/code> covers the &#8220;coordinates + measurements \u2192 fitted coefficients&#8221; scenario. Two distinct responsibilities.<\/p>\n<p dir=\"auto\">This module <strong>does not read any external files<\/strong> (no config dependency).<\/p>\n<p dir=\"auto\"><strong>Pyramid usage example<\/strong>:<\/p>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import json\nfrom pathlib import Path\nfrom wlzpoly import ZernikePolynomials\n\n# (n, m) \u2192 name mapping is sourced from config.json (optional)\ncfg = json.loads(Path(&quot;config.json&quot;).read_text())\nnames = {\n    tuple(int(x) for x in k.split(&quot;,&quot;)): v\n    for k, v in cfg[&quot;zernike_names&quot;].items()\n}\n\n# Default return_type='png' \u2192 bytes ready to write\npng_bytes = ZernikePolynomials.pyramid_image(\n    n_max=4,                          # n=0..4 (15 terms total)\n    names=names,                      # cell labels (j, n, m only when omitted)\n)\nPath(&quot;zernike_pyramid.png&quot;).write_bytes(png_bytes)\n\n# Or get the live Figure for further customization\nfig = ZernikePolynomials.pyramid_image(\n    n_max=4, names=names, return_type='figure',\n)\nfig.savefig(&quot;zernike_pyramid.png&quot;, dpi=130, bbox_inches=&quot;tight&quot;)\"><pre><span class=\"pl-k\">import<\/span> <span class=\"pl-s1\">json<\/span>\n<span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">pathlib<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">Path<\/span>\n<span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">wlzpoly<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">ZernikePolynomials<\/span>\n\n<span class=\"pl-c\"># (n, m) \u2192 name mapping is sourced from config.json (optional)<\/span>\n<span class=\"pl-s1\">cfg<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">json<\/span>.<span class=\"pl-c1\">loads<\/span>(<span class=\"pl-en\">Path<\/span>(<span class=\"pl-s\">\"config.json\"<\/span>).<span class=\"pl-c1\">read_text<\/span>())\n<span class=\"pl-s1\">names<\/span> <span class=\"pl-c1\">=<\/span> {\n    <span class=\"pl-en\">tuple<\/span>(<span class=\"pl-en\">int<\/span>(<span class=\"pl-s1\">x<\/span>) <span class=\"pl-k\">for<\/span> <span class=\"pl-s1\">x<\/span> <span class=\"pl-c1\">in<\/span> <span class=\"pl-s1\">k<\/span>.<span class=\"pl-c1\">split<\/span>(<span class=\"pl-s\">\",\"<\/span>)): <span class=\"pl-s1\">v<\/span>\n    <span class=\"pl-k\">for<\/span> <span class=\"pl-s1\">k<\/span>, <span class=\"pl-s1\">v<\/span> <span class=\"pl-c1\">in<\/span> <span class=\"pl-s1\">cfg<\/span>[<span class=\"pl-s\">\"zernike_names\"<\/span>].<span class=\"pl-c1\">items<\/span>()\n}\n\n<span class=\"pl-c\"># Default return_type='png' \u2192 bytes ready to write<\/span>\n<span class=\"pl-s1\">png_bytes<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-v\">ZernikePolynomials<\/span>.<span class=\"pl-c1\">pyramid_image<\/span>(\n    <span class=\"pl-s1\">n_max<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">4<\/span>,                          <span class=\"pl-c\"># n=0..4 (15 terms total)<\/span>\n    <span class=\"pl-s1\">names<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">names<\/span>,                      <span class=\"pl-c\"># cell labels (j, n, m only when omitted)<\/span>\n)\n<span class=\"pl-en\">Path<\/span>(<span class=\"pl-s\">\"zernike_pyramid.png\"<\/span>).<span class=\"pl-c1\">write_bytes<\/span>(<span class=\"pl-s1\">png_bytes<\/span>)\n\n<span class=\"pl-c\"># Or get the live Figure for further customization<\/span>\n<span class=\"pl-s1\">fig<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-v\">ZernikePolynomials<\/span>.<span class=\"pl-c1\">pyramid_image<\/span>(\n    <span class=\"pl-s1\">n_max<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">4<\/span>, <span class=\"pl-s1\">names<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">names<\/span>, <span class=\"pl-s1\">return_type<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">'figure'<\/span>,\n)\n<span class=\"pl-s1\">fig<\/span>.<span class=\"pl-c1\">savefig<\/span>(<span class=\"pl-s\">\"zernike_pyramid.png\"<\/span>, <span class=\"pl-s1\">dpi<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">130<\/span>, <span class=\"pl-s1\">bbox_inches<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s\">\"tight\"<\/span>)<\/pre><\/div>\n<p dir=\"auto\">Each cell shows the Noll index j, (n, m), and (when supplied) the optical name. The sign of Z_j(\u03c1, \u03b8) on the unit disk is rendered with the RdBu_r colormap. <code>return_type<\/code> selects PNG bytes (for saving \/ HTML embedding) or a live matplotlib Figure.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>regression.py<\/code><\/h3><a id=\"user-content-regressionpy\" class=\"anchor\" aria-label=\"Permalink: regression.py\" href=\"#regressionpy\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">General-purpose linear-regression solvers (no Zernike dependency).<\/p>\n<p dir=\"auto\"><strong>Provided functions<\/strong>:<\/p>\n<ul dir=\"auto\">\n<li><code>fit_lsq(A, T)<\/code> \u2014 plain least squares: \u00e2 = (A\u1d40A)\u207b\u00b9 A\u1d40 T<\/li>\n<li><code>fit_ridge(A, T, lam=\u2026)<\/code> \u2014 Ridge: \u00e2 = (A\u1d40A + \u03bbI)\u207b\u00b9 A\u1d40 T<\/li>\n<li><code>loocv_lambda(A, T, lambdas=\u2026)<\/code> \u2014 LOOCV-driven \u03bb selection<\/li>\n<\/ul>\n<p dir=\"auto\"><code>wlzpoly.decompose<\/code> (Stage 2) imports this module.<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">CLI options<\/h2><a id=\"user-content-cli-options\" class=\"anchor\" aria-label=\"Permalink: CLI options\" href=\"#cli-options\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>generate_samples.py<\/code><\/h3><a id=\"user-content-generate_samplespy\" class=\"anchor\" aria-label=\"Permalink: generate_samples.py\" href=\"#generate_samplespy\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python generate_samples.py [options]\"><pre>python generate_samples.py [options]<\/pre><\/div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Option<\/th>\n<th>Default<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>--noise_sigma<\/code>, <code>-n<\/code><\/td>\n<td>5.0<\/td>\n<td>Gaussian noise standard deviation<\/td>\n<\/tr>\n<tr>\n<td><code>--seed<\/code>, <code>-s<\/code><\/td>\n<td>42<\/td>\n<td>Random seed<\/td>\n<\/tr>\n<tr>\n<td><code>--n_drift<\/code><\/td>\n<td>30<\/td>\n<td>Number of wafers in the drift time series<\/td>\n<\/tr>\n<tr>\n<td><code>--working_folder<\/code><\/td>\n<td><code>Path(__file__).parent<\/code><\/td>\n<td>Base folder for resolving <code>--config_json<\/code> and <code>--wafer_points<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--config_json<\/code><\/td>\n<td><code>\"config.json\"<\/code><\/td>\n<td>Config JSON (resolved under <code>--working_folder<\/code> if relative)<\/td>\n<\/tr>\n<tr>\n<td><code>--wafer_points<\/code><\/td>\n<td><code>\"wafer_points.json\"<\/code><\/td>\n<td>Wafer-points JSON (resolved under <code>--working_folder<\/code> if relative)<\/td>\n<\/tr>\n<tr>\n<td><code>--output_folder<\/code><\/td>\n<td><code>Path(__file__).parent \/ \"samples\"<\/code><\/td>\n<td>Output folder<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\"><strong>Examples<\/strong>:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python generate_samples.py --noise_sigma 0.4 --seed 100\npython generate_samples.py -n 10 --n_drift 50\"><pre>python generate_samples.py --noise_sigma 0.4 --seed 100\npython generate_samples.py -n 10 --n_drift 50<\/pre><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>decompose.py<\/code><\/h3><a id=\"user-content-decomposepy\" class=\"anchor\" aria-label=\"Permalink: decompose.py\" href=\"#decomposepy\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python -m wlzpoly.decompose [options]\"><pre>python -m wlzpoly.decompose [options]<\/pre><\/div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Option<\/th>\n<th>Default<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>--working_folder<\/code><\/td>\n<td><code>Path.cwd()<\/code><\/td>\n<td>Base folder for resolving <code>--wafer_points<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--wafer_points<\/code><\/td>\n<td><code>\"wafer_points.json\"<\/code><\/td>\n<td>Wafer-points JSON (resolved under <code>--working_folder<\/code> if relative)<\/td>\n<\/tr>\n<tr>\n<td><code>--input_file<\/code><\/td>\n<td><code>\"target_file.csv\"<\/code><\/td>\n<td>Measurement CSV (id + P1..PN)<\/td>\n<\/tr>\n<tr>\n<td><code>--n_terms<\/code><\/td>\n<td>9<\/td>\n<td>Number of Zernike polynomial terms (Noll j=1..n_terms); must be \u2264 number of measurement points<\/td>\n<\/tr>\n<tr>\n<td><code>--output_folder<\/code><\/td>\n<td><code>Path.cwd() \/ \"decomposition\"<\/code><\/td>\n<td>Output folder<\/td>\n<\/tr>\n<tr>\n<td><code>--output_file<\/code><\/td>\n<td><code>\"decomposed_targets.csv\"<\/code><\/td>\n<td>Filename for the fitted-coefficients CSV (written inside <code>--output_folder<\/code>)<\/td>\n<\/tr>\n<tr>\n<td><code>--solver<\/code><\/td>\n<td>lsq<\/td>\n<td><code>lsq<\/code> or <code>ridge<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--lam<\/code><\/td>\n<td>0.01<\/td>\n<td>Ridge regularization (used when solver=ridge AND <code>--auto_lam<\/code> is NOT set)<\/td>\n<\/tr>\n<tr>\n<td><code>--auto_lam<\/code><\/td>\n<td>off<\/td>\n<td>When set with <code>--solver ridge<\/code>, picks \u03bb via LOOCV (<code>--lam<\/code> ignored)<\/td>\n<\/tr>\n<tr>\n<td><code>--loocv_lambdas<\/code><\/td>\n<td><code>[0.0, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0]<\/code><\/td>\n<td>Candidate \u03bb values for LOOCV (when <code>--auto_lam<\/code>)<\/td>\n<\/tr>\n<tr>\n<td><code>--loocv_ref<\/code><\/td>\n<td><code>first_wafer<\/code><\/td>\n<td>LOOCV reference T: <code>first_wafer<\/code> \/ <code>mean<\/code> \/ <code>per_wafer<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--coordinate<\/code><\/td>\n<td>cartesian<\/td>\n<td><code>cartesian<\/code> (read x, y) or <code>polar<\/code> (read r, theta)<\/td>\n<\/tr>\n<tr>\n<td><code>--col_wafer_id<\/code><\/td>\n<td><code>\"wafer_id\"<\/code><\/td>\n<td>Name of the wafer-id column in <code>--input_file<\/code> (also used as id column in output CSV)<\/td>\n<\/tr>\n<tr>\n<td><code>--col_points<\/code><\/td>\n<td><code>P1 P2 \u2026 P13<\/code><\/td>\n<td>Measurement-point column names in <code>--input_file<\/code>; must match point ids in <code>--wafer_points<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--coeff_prefix<\/code><\/td>\n<td><code>\"a\"<\/code><\/td>\n<td>Prefix for the coefficient columns in the output CSV (<code>&lt;prefix&gt;1..&lt;prefix&gt;n_terms<\/code>)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">If the columns of <code>--input_file<\/code> do not match <code>--col_wafer_id<\/code> \/ <code>--col_points<\/code>, or the point ids in the <code>--wafer_points<\/code> JSON do not match <code>--col_points<\/code>, <code>parse_args()<\/code> aborts immediately with <code>parser.error<\/code> before main runs.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>verify.py<\/code><\/h3><a id=\"user-content-verifypy\" class=\"anchor\" aria-label=\"Permalink: verify.py\" href=\"#verifypy\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python -m wlzpoly.verify [options]\"><pre>python -m wlzpoly.verify [options]<\/pre><\/div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Option<\/th>\n<th>Default<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>--decomposed_lsq_file<\/code><\/td>\n<td><code>\"decomposed_targets_lsq.csv\"<\/code><\/td>\n<td>LSQ CSV from Stage 2 (id + a1..aN). Skipped if file missing<\/td>\n<\/tr>\n<tr>\n<td><code>--decomposed_ridge_file<\/code><\/td>\n<td><code>\"decomposed_targets_ridge.csv\"<\/code><\/td>\n<td>Ridge CSV from Stage 2. Skipped if file missing<\/td>\n<\/tr>\n<tr>\n<td><code>--ground_truth_file<\/code><\/td>\n<td><code>\"ground_truth_file.csv\"<\/code><\/td>\n<td>Ground-truth CSV (id, scenario, a1..aN)<\/td>\n<\/tr>\n<tr>\n<td><code>--n_terms<\/code><\/td>\n<td>9<\/td>\n<td>Number of Zernike polynomial terms (must match Stage 2 run)<\/td>\n<\/tr>\n<tr>\n<td><code>--scenarios_to_show<\/code><\/td>\n<td>auto (all non-<code>drift<\/code> scenarios)<\/td>\n<td>Scenario labels to include in per-scenario tables\/charts<\/td>\n<\/tr>\n<tr>\n<td><code>--output_folder<\/code><\/td>\n<td><code>Path.cwd() \/ \"verification\"<\/code><\/td>\n<td>Output folder<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>reconstruct.py<\/code> (optional, inverse of <code>decompose<\/code>)<\/h3><a id=\"user-content-reconstructpy-optional-inverse-of-decompose\" class=\"anchor\" aria-label=\"Permalink: reconstruct.py (optional, inverse of decompose)\" href=\"#reconstructpy-optional-inverse-of-decompose\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python -m wlzpoly.reconstruct [options]\"><pre>python -m wlzpoly.reconstruct [options]<\/pre><\/div>\n<p dir=\"auto\">CLI options are split into two argparse groups (<code>Input<\/code> \/ <code>Output<\/code>); <code>-h<\/code> displays them under those headings.<\/p>\n<p dir=\"auto\"><strong>Input<\/strong><\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Option<\/th>\n<th>Default<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>--input_folder<\/code><\/td>\n<td><code>Path.cwd()<\/code><\/td>\n<td>Base folder for resolving <code>--wafer_point_json<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--wafer_point_json<\/code><\/td>\n<td><code>\"wafer_points.json\"<\/code><\/td>\n<td>Wafer-points JSON (must have <code>.json<\/code> extension; resolved under <code>--input_folder<\/code> if relative)<\/td>\n<\/tr>\n<tr>\n<td><code>--decomposed_file<\/code><\/td>\n<td><code>\"decomposed_targets.csv\"<\/code><\/td>\n<td>Decomposed-coefficients CSV (id + <code>&lt;prefix&gt;1..&lt;prefix&gt;N<\/code>)<\/td>\n<\/tr>\n<tr>\n<td><code>--n_terms<\/code><\/td>\n<td>9<\/td>\n<td>Number of Zernike terms to read from <code>--decomposed_file<\/code>. Must equal the count of <code>&lt;prefix&gt;\\d+<\/code> columns in the CSV<\/td>\n<\/tr>\n<tr>\n<td><code>--coordinate<\/code><\/td>\n<td>cartesian<\/td>\n<td><code>cartesian<\/code> (read x, y) or <code>polar<\/code> (read r, theta)<\/td>\n<\/tr>\n<tr>\n<td><code>--col_wafer_id<\/code><\/td>\n<td><code>\"wafer_id\"<\/code><\/td>\n<td>Name of the wafer-id column in <code>--decomposed_file<\/code> (also used as id column in output CSV)<\/td>\n<\/tr>\n<tr>\n<td><code>--col_points<\/code><\/td>\n<td><code>P1 P2 \u2026 P13<\/code><\/td>\n<td>Output measurement-point column names; subset\/permutation of <code>--wafer_point_json<\/code> point ids<\/td>\n<\/tr>\n<tr>\n<td><code>--coeff_prefix<\/code><\/td>\n<td><code>\"a\"<\/code><\/td>\n<td>Prefix for the coefficient columns in <code>--decomposed_file<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--coeff_suffix<\/code><\/td>\n<td><code>\"\"<\/code><\/td>\n<td>Optional trailing tag on the coefficient columns (e.g. <code>_pred<\/code> \/ <code>_true<\/code> produced by ML-pipeline train_output \/ test_output CSVs). Columns are read as <code>&lt;prefix&gt;&lt;j&gt;&lt;suffix&gt;<\/code> and stripped to bare <code>&lt;prefix&gt;&lt;j&gt;<\/code> internally. Empty by default (matches <code>wlzpoly.decompose<\/code> output)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\"><strong>Output<\/strong><\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Option<\/th>\n<th>Default<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>--output_folder<\/code><\/td>\n<td><code>Path.cwd() \/ \"reconstruction\"<\/code><\/td>\n<td>Output folder (CSV is written by the CLI; <code>reconstruct()<\/code> API only returns the DataFrame)<\/td>\n<\/tr>\n<tr>\n<td><code>--output_file<\/code><\/td>\n<td><code>\"reconstructed_targets.csv\"<\/code><\/td>\n<td>Filename for the reconstructed-measurements CSV<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\"><code>parse_args()<\/code> aborts with <code>parser.error<\/code> if any of the following are violated: <code>--wafer_point_json<\/code> does not end in <code>.json<\/code>; <code>--decomposed_file<\/code> or <code>--wafer_point_json<\/code> does not exist; <code>--decomposed_file<\/code> is missing the required <code>--col_wafer_id<\/code> \/ <code>&lt;coeff_prefix&gt;1&lt;coeff_suffix&gt;..&lt;coeff_prefix&gt;N&lt;coeff_suffix&gt;<\/code> columns; the number of <code>&lt;coeff_prefix&gt;\\d+&lt;coeff_suffix&gt;<\/code> columns in <code>--decomposed_file<\/code> does not equal <code>--n_terms<\/code>; any of <code>--col_points<\/code> is not present in the <code>--wafer_point_json<\/code> point ids.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Note on the coordinate option<\/h3><a id=\"user-content-note-on-the-coordinate-option\" class=\"anchor\" aria-label=\"Permalink: Note on the coordinate option\" href=\"#note-on-the-coordinate-option\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\"><code>points_13.json<\/code> stores both (x, y) and (r, theta) for the same 13 points. On a few diagonal edge points the stored <code>r=145<\/code> is rounded (the exact value is \u221a(102.5\u00b2 + 102.5\u00b2) = 144.957).<\/p>\n<ul dir=\"auto\">\n<li><code>--coordinate cartesian<\/code> (default): read <strong>only<\/strong> (x, y) from JSON; convert to polar via the <code>ZernikePolynomials.to_polar(x=\u2026, y=\u2026)<\/code> classmethod, which uses <code>arctan2<\/code> and is numerically exact.<\/li>\n<li><code>--coordinate polar<\/code>: read <strong>only<\/strong> (r, theta) from JSON. Theta is stored in degrees and converted with <code>np.deg2rad<\/code>.<\/li>\n<\/ul>\n<p dir=\"auto\">The RMSE difference between the two modes is at the 4th-decimal level (e.g. a1 LSQ: cartesian 1.5461 vs polar 1.5465). The flag exists so the consumer chooses which JSON field to trust explicitly. <code>decompose.py<\/code>&#8216;s <code>load_wafer_coordinates()<\/code> \/ <code>load_measured_data()<\/code> and the <code>WaferLevelZernikePolynomials<\/code> class are reused by <code>verify.py<\/code>.<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Configuration files<\/h2><a id=\"user-content-configuration-files\" class=\"anchor\" aria-label=\"Permalink: Configuration files\" href=\"#configuration-files\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>config.json<\/code><\/h3><a id=\"user-content-configjson\" class=\"anchor\" aria-label=\"Permalink: config.json\" href=\"#configjson\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Used <strong>only by <code>generate_samples.py<\/code> (Stage 1)<\/strong> for synthetic-data generation. <code>wlzpoly.decompose<\/code> and <code>wlzpoly.verify<\/code> do not read it \u2014 their per-run knobs (<code>n_terms<\/code>, <code>loocv_lambdas<\/code>, <code>scenarios_to_show<\/code>) are CLI flags instead.<\/p>\n<div class=\"highlight highlight-source-json notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"{\n  &quot;wafer&quot;: {\n    &quot;size_mm&quot;: 300,\n    &quot;edge_exclusion_mm&quot;: 5,\n    &quot;fit_radius_mm&quot;: 145\n  },\n\n  &quot;scenarios&quot;: {\n    &quot;normal&quot;:     {&quot;1&quot;: 500.0, &quot;2&quot;: 0.5, ..., &quot;9&quot;: 0.0},\n    &quot;tilted&quot;:     {&quot;1&quot;: 498.0, &quot;2&quot;: 8.0, ...},\n    &quot;bowl&quot;:       {...},\n    &quot;astigmatic&quot;: {...},\n    &quot;trefoil&quot;:    {...},\n    &quot;comatic&quot;:    {...}\n  },\n\n  &quot;drift_series&quot;: {\n    &quot;piston_decay_per_step&quot;: -0.15,\n    &quot;bowl_deepen_per_step&quot;:  -0.10,\n    &quot;piston_random_sigma&quot;:    0.4,\n    &quot;bowl_random_sigma&quot;:      0.3,\n    &quot;shape_random_sigma&quot;:     0.2\n  },\n\n  &quot;decomposition&quot;: {\n    &quot;n_terms&quot;: 9,\n    &quot;loocv_lambdas&quot;: [0.0, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0],\n    &quot;scenarios_to_show&quot;: [\n      &quot;normal&quot;, &quot;tilted&quot;, &quot;bowl&quot;,\n      &quot;astigmatic&quot;, &quot;trefoil&quot;, &quot;comatic&quot;\n    ]\n  },\n\n  &quot;zernike_names&quot;: {\n    &quot;0,0&quot;:  &quot;Piston&quot;,\n    &quot;1,1&quot;:  &quot;Tilt X&quot;,\n    &quot;1,-1&quot;: &quot;Tilt Y&quot;,\n    ...\n  }\n}\"><pre>{\n  <span class=\"pl-ent\">\"wafer\"<\/span>: {\n    <span class=\"pl-ent\">\"size_mm\"<\/span>: <span class=\"pl-c1\">300<\/span>,\n    <span class=\"pl-ent\">\"edge_exclusion_mm\"<\/span>: <span class=\"pl-c1\">5<\/span>,\n    <span class=\"pl-ent\">\"fit_radius_mm\"<\/span>: <span class=\"pl-c1\">145<\/span>\n  },\n\n  <span class=\"pl-ent\">\"scenarios\"<\/span>: {\n    <span class=\"pl-ent\">\"normal\"<\/span>:     {<span class=\"pl-ent\">\"1\"<\/span>: <span class=\"pl-c1\">500.0<\/span>, <span class=\"pl-ent\">\"2\"<\/span>: <span class=\"pl-c1\">0.5<\/span>, <span class=\"pl-ii\">..., \"9\": 0.0<\/span>},\n    <span class=\"pl-ent\">\"tilted\"<\/span>:     {<span class=\"pl-ent\">\"1\"<\/span>: <span class=\"pl-c1\">498.0<\/span>, <span class=\"pl-ent\">\"2\"<\/span>: <span class=\"pl-c1\">8.0<\/span>, <span class=\"pl-ii\">...<\/span>},\n    <span class=\"pl-ent\">\"bowl\"<\/span>:       {<span class=\"pl-ii\">...<\/span>},\n    <span class=\"pl-ent\">\"astigmatic\"<\/span>: {<span class=\"pl-ii\">...<\/span>},\n    <span class=\"pl-ent\">\"trefoil\"<\/span>:    {<span class=\"pl-ii\">...<\/span>},\n    <span class=\"pl-ent\">\"comatic\"<\/span>:    {<span class=\"pl-ii\">...<\/span>}\n  },\n\n  <span class=\"pl-ent\">\"drift_series\"<\/span>: {\n    <span class=\"pl-ent\">\"piston_decay_per_step\"<\/span>: <span class=\"pl-c1\">-0.15<\/span>,\n    <span class=\"pl-ent\">\"bowl_deepen_per_step\"<\/span>:  <span class=\"pl-c1\">-0.10<\/span>,\n    <span class=\"pl-ent\">\"piston_random_sigma\"<\/span>:    <span class=\"pl-c1\">0.4<\/span>,\n    <span class=\"pl-ent\">\"bowl_random_sigma\"<\/span>:      <span class=\"pl-c1\">0.3<\/span>,\n    <span class=\"pl-ent\">\"shape_random_sigma\"<\/span>:     <span class=\"pl-c1\">0.2<\/span>\n  },\n\n  <span class=\"pl-ent\">\"decomposition\"<\/span>: {\n    <span class=\"pl-ent\">\"n_terms\"<\/span>: <span class=\"pl-c1\">9<\/span>,\n    <span class=\"pl-ent\">\"loocv_lambdas\"<\/span>: [<span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-c1\">0.001<\/span>, <span class=\"pl-c1\">0.01<\/span>, <span class=\"pl-c1\">0.1<\/span>, <span class=\"pl-c1\">1.0<\/span>, <span class=\"pl-c1\">10.0<\/span>, <span class=\"pl-c1\">100.0<\/span>],\n    <span class=\"pl-ent\">\"scenarios_to_show\"<\/span>: [\n      <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>normal<span class=\"pl-pds\">\"<\/span><\/span>, <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>tilted<span class=\"pl-pds\">\"<\/span><\/span>, <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>bowl<span class=\"pl-pds\">\"<\/span><\/span>,\n      <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>astigmatic<span class=\"pl-pds\">\"<\/span><\/span>, <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>trefoil<span class=\"pl-pds\">\"<\/span><\/span>, <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>comatic<span class=\"pl-pds\">\"<\/span><\/span>\n    ]\n  },\n\n  <span class=\"pl-ent\">\"zernike_names\"<\/span>: {\n    <span class=\"pl-ent\">\"0,0\"<\/span>:  <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>Piston<span class=\"pl-pds\">\"<\/span><\/span>,\n    <span class=\"pl-ent\">\"1,1\"<\/span>:  <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>Tilt X<span class=\"pl-pds\">\"<\/span><\/span>,\n    <span class=\"pl-ent\">\"1,-1\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>Tilt Y<span class=\"pl-pds\">\"<\/span><\/span>,\n    <span class=\"pl-ii\">...<\/span>\n  }\n}<\/pre><\/div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Section<\/th>\n<th>Used by<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>wafer<\/code><\/td>\n<td>(informational)<\/td>\n<td>Wafer size + edge exclusion + fitting radius<\/td>\n<\/tr>\n<tr>\n<td><code>scenarios<\/code><\/td>\n<td>Stage 1<\/td>\n<td>Six ground-truth scenarios (a\u2081..a\u2089 coefficients)<\/td>\n<\/tr>\n<tr>\n<td><code>drift_series<\/code><\/td>\n<td>Stage 1<\/td>\n<td>Time-series drift parameters (decay + random walk \u03c3)<\/td>\n<\/tr>\n<tr>\n<td><code>decomposition.n_terms<\/code><\/td>\n<td>Stage 1<\/td>\n<td>Zernike order for ground-truth generation. Stage 2\/3 use <code>--n_terms<\/code> CLI instead<\/td>\n<\/tr>\n<tr>\n<td><code>decomposition.loocv_lambdas<\/code>, <code>scenarios_to_show<\/code><\/td>\n<td>(legacy)<\/td>\n<td>No longer read by Stage 2\/3 \u2014 use <code>--loocv_lambdas<\/code> \/ <code>--scenarios_to_show<\/code> CLI flags<\/td>\n<\/tr>\n<tr>\n<td><code>zernike_names<\/code><\/td>\n<td>(legacy)<\/td>\n<td>No longer read by Stage 2\/3 \u2014 the standard (n, m) \u2192 name map is hardcoded in <code>wlzpoly.verify<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>points_13.json<\/code><\/h3><a id=\"user-content-points_13json\" class=\"anchor\" aria-label=\"Permalink: points_13.json\" href=\"#points_13json\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">13-point measurement coordinate definition (cardinal-aligned pattern).<\/p>\n<div class=\"highlight highlight-source-json notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"{\n  &quot;wafer_size_mm&quot;: 300,\n  &quot;edge_exclusion_mm&quot;: 5,\n  &quot;wafer_radius_mm&quot;: 145,\n  &quot;pattern&quot;: &quot;13-point cardinal-aligned&quot;,\n  &quot;points&quot;: [\n    {&quot;id&quot;: &quot;P1&quot;,  &quot;x&quot;: 0,    &quot;y&quot;: 0,    &quot;r&quot;: 0,   &quot;theta&quot;: 0,   &quot;zone&quot;: &quot;Center&quot;},\n    {&quot;id&quot;: &quot;P2&quot;,  &quot;x&quot;: 75,   &quot;y&quot;: 0,    &quot;r&quot;: 75,  &quot;theta&quot;: 0,   &quot;zone&quot;: &quot;Mid_E&quot;},\n    ...\n    {&quot;id&quot;: &quot;P13&quot;, &quot;x&quot;: 102.5, &quot;y&quot;: -102.5, &quot;r&quot;: 145, &quot;theta&quot;: 315, &quot;zone&quot;: &quot;Edge_SE&quot;}\n  ]\n}\"><pre>{\n  <span class=\"pl-ent\">\"wafer_size_mm\"<\/span>: <span class=\"pl-c1\">300<\/span>,\n  <span class=\"pl-ent\">\"edge_exclusion_mm\"<\/span>: <span class=\"pl-c1\">5<\/span>,\n  <span class=\"pl-ent\">\"wafer_radius_mm\"<\/span>: <span class=\"pl-c1\">145<\/span>,\n  <span class=\"pl-ent\">\"pattern\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>13-point cardinal-aligned<span class=\"pl-pds\">\"<\/span><\/span>,\n  <span class=\"pl-ent\">\"points\"<\/span>: [\n    {<span class=\"pl-ent\">\"id\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>P1<span class=\"pl-pds\">\"<\/span><\/span>,  <span class=\"pl-ent\">\"x\"<\/span>: <span class=\"pl-c1\">0<\/span>,    <span class=\"pl-ent\">\"y\"<\/span>: <span class=\"pl-c1\">0<\/span>,    <span class=\"pl-ent\">\"r\"<\/span>: <span class=\"pl-c1\">0<\/span>,   <span class=\"pl-ent\">\"theta\"<\/span>: <span class=\"pl-c1\">0<\/span>,   <span class=\"pl-ent\">\"zone\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>Center<span class=\"pl-pds\">\"<\/span><\/span>},\n    {<span class=\"pl-ent\">\"id\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>P2<span class=\"pl-pds\">\"<\/span><\/span>,  <span class=\"pl-ent\">\"x\"<\/span>: <span class=\"pl-c1\">75<\/span>,   <span class=\"pl-ent\">\"y\"<\/span>: <span class=\"pl-c1\">0<\/span>,    <span class=\"pl-ent\">\"r\"<\/span>: <span class=\"pl-c1\">75<\/span>,  <span class=\"pl-ent\">\"theta\"<\/span>: <span class=\"pl-c1\">0<\/span>,   <span class=\"pl-ent\">\"zone\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>Mid_E<span class=\"pl-pds\">\"<\/span><\/span>},\n    <span class=\"pl-ii\">...<\/span>\n    {<span class=\"pl-ent\">\"id\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>P13<span class=\"pl-pds\">\"<\/span><\/span>, <span class=\"pl-ent\">\"x\"<\/span>: <span class=\"pl-c1\">102.5<\/span>, <span class=\"pl-ent\">\"y\"<\/span>: <span class=\"pl-c1\">-102.5<\/span>, <span class=\"pl-ent\">\"r\"<\/span>: <span class=\"pl-c1\">145<\/span>, <span class=\"pl-ent\">\"theta\"<\/span>: <span class=\"pl-c1\">315<\/span>, <span class=\"pl-ent\">\"zone\"<\/span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>Edge_SE<span class=\"pl-pds\">\"<\/span><\/span>}\n  ]\n}<\/pre><\/div>\n<p dir=\"auto\">The 13-point layout:<\/p>\n<ul dir=\"auto\">\n<li><strong>P1<\/strong>: Center (1)<\/li>\n<li><strong>P2..P5<\/strong>: Middle ring r=75mm at 0\u00b0\/90\u00b0\/180\u00b0\/270\u00b0 (4)<\/li>\n<li><strong>P6..P13<\/strong>: Edge ring r=145mm at 0\u00b0\/45\u00b0\/&#8230;\/315\u00b0 (8)<\/li>\n<\/ul>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Output files explained<\/h2><a id=\"user-content-output-files-explained\" class=\"anchor\" aria-label=\"Permalink: Output files explained\" href=\"#output-files-explained\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>target_file.csv<\/code> (Stage 1 output)<\/h3><a id=\"user-content-target_filecsv-stage-1-output\" class=\"anchor\" aria-label=\"Permalink: target_file.csv (Stage 1 output)\" href=\"#target_filecsv-stage-1-output\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"id,P1,P2,P3,...,P13\nW_01,504.99,497.06,504.92,...,502.55\nW_02,507.97,510.82,493.97,...,506.47\n...\"><pre class=\"notranslate\"><code>id,P1,P2,P3,...,P13\nW_01,504.99,497.06,504.92,...,502.55\nW_02,507.97,510.82,493.97,...,506.47\n...\n<\/code><\/pre><\/div>\n<p dir=\"auto\">13-point thickness measurements \u2014 same shape as a real metrology export.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>ground_truth.csv<\/code> (Stage 1 output)<\/h3><a id=\"user-content-ground_truthcsv-stage-1-output\" class=\"anchor\" aria-label=\"Permalink: ground_truth.csv (Stage 1 output)\" href=\"#ground_truthcsv-stage-1-output\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"id,scenario,a1,a2,a3,...,a9\nW_01,normal,500.0,0.5,-0.3,...,0.0\nW_02,tilted,498.0,8.0,-1.5,...,0.0\"><pre class=\"notranslate\"><code>id,scenario,a1,a2,a3,...,a9\nW_01,normal,500.0,0.5,-0.3,...,0.0\nW_02,tilted,498.0,8.0,-1.5,...,0.0\n<\/code><\/pre><\/div>\n<p dir=\"auto\">Per-wafer ground-truth Zernike coefficients (used only for verification).<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>decomposed_targets.csv<\/code> (Stage 2 output)<\/h3><a id=\"user-content-decomposed_targetscsv-stage-2-output\" class=\"anchor\" aria-label=\"Permalink: decomposed_targets.csv (Stage 2 output)\" href=\"#decomposed_targetscsv-stage-2-output\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"id,a1,a2,a3,...,a9\nW_01,499.27,-1.99,2.16,...,0.21\nW_02,498.50,8.05,-1.61,...,-0.05\"><pre class=\"notranslate\"><code>id,a1,a2,a3,...,a9\nW_01,499.27,-1.99,2.16,...,0.21\nW_02,498.50,8.05,-1.61,...,-0.05\n<\/code><\/pre><\/div>\n<p dir=\"auto\">LSQ-recovered coefficients \u2014 the 13 \u2192 9 compression result (production output).<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>decomposition_results.csv<\/code> (Stage 3 output)<\/h3><a id=\"user-content-decomposition_resultscsv-stage-3-output\" class=\"anchor\" aria-label=\"Permalink: decomposition_results.csv (Stage 3 output)\" href=\"#decomposition_resultscsv-stage-3-output\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"id,scenario,a1_true,a1_lsq,a1_ridge,a2_true,a2_lsq,a2_ridge,...\nW_01,normal,500.0,499.27,499.20,0.5,-1.99,-1.95,...\"><pre class=\"notranslate\"><code>id,scenario,a1_true,a1_lsq,a1_ridge,a2_true,a2_lsq,a2_ridge,...\nW_01,normal,500.0,499.27,499.20,0.5,-1.99,-1.95,...\n<\/code><\/pre><\/div>\n<p dir=\"auto\">For every coefficient, three columns: truth \/ LSQ \/ Ridge \u2192 27 columns + 2 metadata.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>decomposition_summary_*.png<\/code> (Stage 3 output)<\/h3><a id=\"user-content-decomposition_summary_png-stage-3-output\" class=\"anchor\" aria-label=\"Permalink: decomposition_summary_*.png (Stage 3 output)\" href=\"#decomposition_summary_png-stage-3-output\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Six scenario wafers, one row each. Every row has three panels:<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  W_01        \u2502   mean (a1)   \u2502   shape components (a2..a9)    \u2502\n\u2502  normal      \u2502   [narrow bar]\u2502   [eight wide bars]             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\"><pre class=\"notranslate\"><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  W_01        \u2502   mean (a1)   \u2502   shape components (a2..a9)    \u2502\n\u2502  normal      \u2502   [narrow bar]\u2502   [eight wide bars]             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre><\/div>\n<ul dir=\"auto\">\n<li><strong>Left<\/strong>: id + scenario label<\/li>\n<li><strong>Middle<\/strong>: a\u2081 (Piston, mean thickness) \u2014 y-axis zoomed to \u00b15<\/li>\n<li><strong>Right<\/strong>: a\u2082..a\u2089 (shape components)<\/li>\n<\/ul>\n<p dir=\"auto\">Every bar panel pairs navy (truth) with orange (fitted). The closer the bars overlap, the more accurate the fit.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Console output sample (<code>verify.py<\/code>)<\/h3><a id=\"user-content-console-output-sample-verifypy\" class=\"anchor\" aria-label=\"Permalink: Console output sample (verify.py)\" href=\"#console-output-sample-verifypy\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"Per-coefficient RMSE across 36 samples:\ncoef          RMSE (LSQ)    RMSE (Ridge)\n----------------------------------------\na1 (Piston  )    1.5464        1.6408\na2 (Tilt X  )    1.6980        1.6955\n...\na9 (Trefoil Y)   0.8764        0.8762\n\n\u03bb used for Ridge: 0.01\"><pre class=\"notranslate\"><code>Per-coefficient RMSE across 36 samples:\ncoef          RMSE (LSQ)    RMSE (Ridge)\n----------------------------------------\na1 (Piston  )    1.5464        1.6408\na2 (Tilt X  )    1.6980        1.6955\n...\na9 (Trefoil Y)   0.8764        0.8762\n\n\u03bb used for Ridge: 0.01\n<\/code><\/pre><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>reconstructed_targets.csv<\/code> (Stage 4 output)<\/h3><a id=\"user-content-reconstructed_targetscsv-stage-4-output\" class=\"anchor\" aria-label=\"Permalink: reconstructed_targets.csv (Stage 4 output)\" href=\"#reconstructed_targetscsv-stage-4-output\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"id,P1,P2,P3,...,P13\nW_01,502.56,495.79,507.85,...,495.94\nW_02,503.00,510.27,497.91,...,505.76\n...\"><pre class=\"notranslate\"><code>id,P1,P2,P3,...,P13\nW_01,502.56,495.79,507.85,...,495.94\nW_02,503.00,510.27,497.91,...,505.76\n...\n<\/code><\/pre><\/div>\n<p dir=\"auto\">Same wide shape as <code>target_file.csv<\/code> from Stage 1 (<code>id + P1..PN<\/code>). Produced by <code>wlzpoly.reconstruct<\/code> from a Stage 2 decomposed-coefficients CSV via <code>T_recon = A \u00b7 a<\/code>, with no noise added back. The per-point residual <code>target_file \u2212 reconstructed_targets<\/code> is the part that the first N Zernike basis functions could not absorb (noise + truncation error). <code>run_demo.ps1<\/code> writes two files \u2014 <code>reconstructed_lsq.csv<\/code> and <code>reconstructed_ridge.csv<\/code> \u2014 mirroring the Stage 2 fits.<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Scenario reference<\/h2><a id=\"user-content-scenario-reference\" class=\"anchor\" aria-label=\"Permalink: Scenario reference\" href=\"#scenario-reference\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Six ground-truth scenarios defined in <code>config.json<\/code>:<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Scenario<\/th>\n<th>Dominant coefficient<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>normal<\/td>\n<td>a\u2084 = -2<\/td>\n<td>Nominal (mild bowl)<\/td>\n<\/tr>\n<tr>\n<td>tilted<\/td>\n<td><strong>a\u2082 = +8<\/strong><\/td>\n<td>Chuck level error (Tilt X)<\/td>\n<\/tr>\n<tr>\n<td>bowl<\/td>\n<td><strong>a\u2084 = -12<\/strong><\/td>\n<td>Center-to-edge imbalance (deep Defocus)<\/td>\n<\/tr>\n<tr>\n<td>astigmatic<\/td>\n<td><strong>a\u2086 = +6.5<\/strong><\/td>\n<td>Showerhead 0\/90 asymmetry<\/td>\n<\/tr>\n<tr>\n<td>trefoil<\/td>\n<td><strong>a\u2089 = +4.5<\/strong><\/td>\n<td>3-zone heater issue<\/td>\n<\/tr>\n<tr>\n<td>comatic<\/td>\n<td><strong>a\u2088 = +3.8<\/strong><\/td>\n<td>X-direction asymmetric flow<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">Total: 36 wafers = 6 scenarios + 30 drift samples (drift = a\u2081 decay + a\u2084 deepening + random walk on the others).<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Recipes<\/h2><a id=\"user-content-recipes\" class=\"anchor\" aria-label=\"Permalink: Recipes\" href=\"#recipes\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Change noise strength<\/h3><a id=\"user-content-change-noise-strength\" class=\"anchor\" aria-label=\"Permalink: Change noise strength\" href=\"#change-noise-strength\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Edit <code>run_demo.ps1<\/code> Stage 1 line \u2014 change <code>--noise_sigma 5.0<\/code> to the desired value \u2014 then <code>.\\run_demo.ps1<\/code>. Or call manually (run from <code>examples\/<\/code>):<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Low noise (LSQ recovers near-perfectly)\npython generate_samples.py --working_folder . \\\n    --config_json .\/configuration\/config.json \\\n    --wafer_points .\/configuration\/points_13.json \\\n    --output_folder .\/1_samples --noise_sigma 0.4\n\n# Stage 2a: LSQ\npython -m wlzpoly.decompose --working_folder . \\\n    --wafer_points .\/1_samples\/points_13.json \\\n    --input_file .\/1_samples\/target_file.csv \\\n    --output_folder .\/2_decomposition \\\n    --output_file decomposed_targets_lsq.csv \\\n    --n_terms 9 --solver lsq\n\n# Stage 2b: Ridge with LOOCV\npython -m wlzpoly.decompose --working_folder . \\\n    --wafer_points .\/1_samples\/points_13.json \\\n    --input_file .\/1_samples\/target_file.csv \\\n    --output_folder .\/2_decomposition \\\n    --output_file decomposed_targets_ridge.csv \\\n    --n_terms 9 --solver ridge --auto_lam --loocv_ref first_wafer\n\n# Stage 3\npython -m wlzpoly.verify \\\n    --decomposed_lsq_file .\/2_decomposition\/decomposed_targets_lsq.csv \\\n    --decomposed_ridge_file .\/2_decomposition\/decomposed_targets_ridge.csv \\\n    --ground_truth_file .\/1_samples\/ground_truth.csv \\\n    --n_terms 9 --output_folder .\/3_verification\n\n# Stage 4a: LSQ reconstruction\npython -m wlzpoly.reconstruct --input_folder . \\\n    --wafer_point_json .\/1_samples\/points_13.json \\\n    --decomposed_file .\/2_decomposition\/decomposed_targets_lsq.csv \\\n    --output_folder .\/4_reconstruction \\\n    --output_file reconstructed_lsq.csv \\\n    --n_terms 9 --col_wafer_id id\n\n# Stage 4b: Ridge reconstruction\npython -m wlzpoly.reconstruct --input_folder . \\\n    --wafer_point_json .\/1_samples\/points_13.json \\\n    --decomposed_file .\/2_decomposition\/decomposed_targets_ridge.csv \\\n    --output_folder .\/4_reconstruction \\\n    --output_file reconstructed_ridge.csv \\\n    --n_terms 9 --col_wafer_id id\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Low noise (LSQ recovers near-perfectly)<\/span>\npython generate_samples.py --working_folder <span class=\"pl-c1\">.<\/span> \\\n    --config_json .\/configuration\/config.json \\\n    --wafer_points .\/configuration\/points_13.json \\\n    --output_folder .\/1_samples --noise_sigma 0.4\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 2a: LSQ<\/span>\npython -m wlzpoly.decompose --working_folder <span class=\"pl-c1\">.<\/span> \\\n    --wafer_points .\/1_samples\/points_13.json \\\n    --input_file .\/1_samples\/target_file.csv \\\n    --output_folder .\/2_decomposition \\\n    --output_file decomposed_targets_lsq.csv \\\n    --n_terms 9 --solver lsq\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 2b: Ridge with LOOCV<\/span>\npython -m wlzpoly.decompose --working_folder <span class=\"pl-c1\">.<\/span> \\\n    --wafer_points .\/1_samples\/points_13.json \\\n    --input_file .\/1_samples\/target_file.csv \\\n    --output_folder .\/2_decomposition \\\n    --output_file decomposed_targets_ridge.csv \\\n    --n_terms 9 --solver ridge --auto_lam --loocv_ref first_wafer\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 3<\/span>\npython -m wlzpoly.verify \\\n    --decomposed_lsq_file .\/2_decomposition\/decomposed_targets_lsq.csv \\\n    --decomposed_ridge_file .\/2_decomposition\/decomposed_targets_ridge.csv \\\n    --ground_truth_file .\/1_samples\/ground_truth.csv \\\n    --n_terms 9 --output_folder .\/3_verification\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 4a: LSQ reconstruction<\/span>\npython -m wlzpoly.reconstruct --input_folder <span class=\"pl-c1\">.<\/span> \\\n    --wafer_point_json .\/1_samples\/points_13.json \\\n    --decomposed_file .\/2_decomposition\/decomposed_targets_lsq.csv \\\n    --output_folder .\/4_reconstruction \\\n    --output_file reconstructed_lsq.csv \\\n    --n_terms 9 --col_wafer_id id\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Stage 4b: Ridge reconstruction<\/span>\npython -m wlzpoly.reconstruct --input_folder <span class=\"pl-c1\">.<\/span> \\\n    --wafer_point_json .\/1_samples\/points_13.json \\\n    --decomposed_file .\/2_decomposition\/decomposed_targets_ridge.csv \\\n    --output_folder .\/4_reconstruction \\\n    --output_file reconstructed_ridge.csv \\\n    --n_terms 9 --col_wafer_id id<\/pre><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Add a new scenario<\/h3><a id=\"user-content-add-a-new-scenario\" class=\"anchor\" aria-label=\"Permalink: Add a new scenario\" href=\"#add-a-new-scenario\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Append to <code>config.json<\/code>:<\/p>\n<div class=\"highlight highlight-source-json notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"&quot;scenarios&quot;: {\n  ...\n  &quot;spherical_issue&quot;: {\n    &quot;1&quot;: 500.0, &quot;2&quot;: 0.0, &quot;3&quot;: 0.0, &quot;4&quot;: -1.0,\n    &quot;5&quot;: 0.0, &quot;6&quot;: 0.0, &quot;7&quot;: 0.0, &quot;8&quot;: 0.0, &quot;9&quot;: 0.0\n  }\n}\"><pre><span class=\"pl-ent\">\"scenarios\"<\/span>: {\n  <span class=\"pl-ii\">...<\/span>\n  <span class=\"pl-ent\">\"spherical_issue\"<\/span>: {\n    <span class=\"pl-ent\">\"1\"<\/span>: <span class=\"pl-c1\">500.0<\/span>, <span class=\"pl-ent\">\"2\"<\/span>: <span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-ent\">\"3\"<\/span>: <span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-ent\">\"4\"<\/span>: <span class=\"pl-c1\">-1.0<\/span>,\n    <span class=\"pl-ent\">\"5\"<\/span>: <span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-ent\">\"6\"<\/span>: <span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-ent\">\"7\"<\/span>: <span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-ent\">\"8\"<\/span>: <span class=\"pl-c1\">0.0<\/span>, <span class=\"pl-ent\">\"9\"<\/span>: <span class=\"pl-c1\">0.0<\/span>\n  }\n}<\/pre><\/div>\n<p dir=\"auto\">No code change required.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Extend the Zernike order<\/h3><a id=\"user-content-extend-the-zernike-order\" class=\"anchor\" aria-label=\"Permalink: Extend the Zernike order\" href=\"#extend-the-zernike-order\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Three places matter:<\/p>\n<ol dir=\"auto\">\n<li>\n<p dir=\"auto\"><strong>Stage 1<\/strong> (ground-truth generation): bump <code>cfg[\"decomposition\"][\"n_terms\"]<\/code> in <code>config.json<\/code> so <code>ground_truth.csv<\/code> carries <code>a1..a11<\/code> instead of <code>a1..a9<\/code>.<\/p>\n<\/li>\n<li>\n<p dir=\"auto\"><strong>Stage 2 \/ 3<\/strong> (fitting + verification): pass <code>--n_terms 11<\/code> on the CLI. The flag is the only authority for the fitter \u2014 neither module reads <code>config.json<\/code>.<\/p>\n<\/li>\n<li>\n<p dir=\"auto\"><strong>Stage 4<\/strong> (reconstruction): pass the <strong>same<\/strong> <code>--n_terms 11<\/code> to <code>wlzpoly.reconstruct<\/code>. The CLI rejects any mismatch between <code>--n_terms<\/code> and the <code>&lt;coeff_prefix&gt;\\d+<\/code> column count in <code>--decomposed_file<\/code>, so Stages 2 and 4 must agree.<\/p>\n<\/li>\n<\/ol>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python -m wlzpoly.decompose    ... --n_terms 11\npython -m wlzpoly.verify       ... --n_terms 11\npython -m wlzpoly.reconstruct  ... --n_terms 11\"><pre>python -m wlzpoly.decompose    ... --n_terms 11\npython -m wlzpoly.verify       ... --n_terms 11\npython -m wlzpoly.reconstruct  ... --n_terms 11<\/pre><\/div>\n<p dir=\"auto\">Maximum <code>n_terms<\/code> is the number of measurement points (13 here). Exceeding it makes <code>A\u1d40A<\/code> singular and the coefficients diverge \u2014 leave at least 4 residual DOF for stable fits.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Change the measurement layout<\/h3><a id=\"user-content-change-the-measurement-layout\" class=\"anchor\" aria-label=\"Permalink: Change the measurement layout\" href=\"#change-the-measurement-layout\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Edit <code>points_13.json<\/code> directly (coordinates and zones are user-editable). No code change required.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Write outputs to a different folder<\/h3><a id=\"user-content-write-outputs-to-a-different-folder\" class=\"anchor\" aria-label=\"Permalink: Write outputs to a different folder\" href=\"#write-outputs-to-a-different-folder\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python -m wlzpoly.verify --output_folder my_results\"><pre>python -m wlzpoly.verify --output_folder my_results<\/pre><\/div>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Algorithm summary<\/h2><a id=\"user-content-algorithm-summary\" class=\"anchor\" aria-label=\"Permalink: Algorithm summary\" href=\"#algorithm-summary\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Zernike decomposition<\/h3><a id=\"user-content-zernike-decomposition\" class=\"anchor\" aria-label=\"Permalink: Zernike decomposition\" href=\"#zernike-decomposition\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Wafer thickness is modeled as a sum of Zernike polynomials on the unit disk:<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"T(\u03c1, \u03b8) = \u03a3_{k=1..N} a_k \u00b7 Z_k(\u03c1, \u03b8) + \u03b5\"><pre class=\"notranslate\"><code>T(\u03c1, \u03b8) = \u03a3_{k=1..N} a_k \u00b7 Z_k(\u03c1, \u03b8) + \u03b5\n<\/code><\/pre><\/div>\n<p dir=\"auto\">where:<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Symbol<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>T(\u03c1, \u03b8)<\/code><\/td>\n<td>wafer thickness at a point on the unit disk (the quantity being decomposed)<\/td>\n<\/tr>\n<tr>\n<td><code>\u03c1<\/code><\/td>\n<td>normalized radial coordinate; \u03c1 = 0 at the wafer center, \u03c1 = 1 at the fitting-radius edge<\/td>\n<\/tr>\n<tr>\n<td><code>\u03b8<\/code><\/td>\n<td>azimuthal angle, in radians, measured CCW from the +x axis<\/td>\n<\/tr>\n<tr>\n<td><code>N<\/code><\/td>\n<td>number of Zernike terms retained in the expansion (= the <code>--n_terms<\/code> CLI flag)<\/td>\n<\/tr>\n<tr>\n<td><code>k<\/code><\/td>\n<td>Noll index, k = 1 .. N<\/td>\n<\/tr>\n<tr>\n<td><code>a_k<\/code><\/td>\n<td>k-th Zernike coefficient (Noll index k), recovered by the fitter<\/td>\n<\/tr>\n<tr>\n<td><code>Z_k(\u03c1, \u03b8)<\/code><\/td>\n<td>k-th Zernike polynomial in the Noll convention<\/td>\n<\/tr>\n<tr>\n<td><code>\u03b5<\/code><\/td>\n<td>measurement noise (per-point residual not captured by the first N basis functions)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">Sampled at the 13 measurement points, this becomes a linear system:<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"T = A \u00b7 a + \u03b5     (T: 13\u00d71, A: 13\u00d79, a: 9\u00d71)\"><pre class=\"notranslate\"><code>T = A \u00b7 a + \u03b5     (T: 13\u00d71, A: 13\u00d79, a: 9\u00d71)\n<\/code><\/pre><\/div>\n<p dir=\"auto\">where:<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Symbol<\/th>\n<th>Shape<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>T<\/code><\/td>\n<td>13\u00d71<\/td>\n<td>measured thickness vector; <code>T[i]<\/code> = thickness at measurement point i<\/td>\n<\/tr>\n<tr>\n<td><code>A<\/code><\/td>\n<td>13\u00d79<\/td>\n<td>basis matrix; <code>A[i, k] = Z_k(\u03c1_i, \u03b8_i)<\/code> \u2014 k-th Zernike polynomial evaluated at the i-th measurement point<\/td>\n<\/tr>\n<tr>\n<td><code>a<\/code><\/td>\n<td>9\u00d71<\/td>\n<td>Zernike coefficient vector; <code>a[k] = a_k<\/code>, the unknown to be recovered by the fitter<\/td>\n<\/tr>\n<tr>\n<td><code>\u03b5<\/code><\/td>\n<td>13\u00d71<\/td>\n<td>per-point measurement noise (residual not captured by the first 9 basis functions)<\/td>\n<\/tr>\n<tr>\n<td><code>13<\/code><\/td>\n<td>\u2014<\/td>\n<td>number of measurement points (= rows of <code>A<\/code> and <code>T<\/code>); set by <code>--wafer_points<\/code> JSON<\/td>\n<\/tr>\n<tr>\n<td><code>9<\/code><\/td>\n<td>\u2014<\/td>\n<td>number of Zernike terms (= columns of <code>A<\/code> = rows of <code>a<\/code>); set by <code>--n_terms<\/code> (default 9)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Zernike reconstruction<\/h3><a id=\"user-content-zernike-reconstruction\" class=\"anchor\" aria-label=\"Permalink: Zernike reconstruction\" href=\"#zernike-reconstruction\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">Reconstruction is the <strong>inverse of decomposition<\/strong>: given an already-known Zernike coefficient vector <code>a<\/code>, regenerate the N-point measurement profile it describes. This is what <a href=\"#reconstructpy-optional-inverse-of-decompose\"><code>wlzpoly.reconstruct<\/code><\/a> (Stage 4, optional) does.<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"T_recon = A \u00b7 a            (no \u03b5; reconstructed signal is noise-free by construction)\"><pre class=\"notranslate\"><code>T_recon = A \u00b7 a            (no \u03b5; reconstructed signal is noise-free by construction)\n<\/code><\/pre><\/div>\n<p dir=\"auto\">where:<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Symbol<\/th>\n<th>Shape<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>T_recon<\/code><\/td>\n<td>13\u00d71<\/td>\n<td>reconstructed thickness vector at the same N measurement points<\/td>\n<\/tr>\n<tr>\n<td><code>A<\/code><\/td>\n<td>13\u00d79<\/td>\n<td>same basis matrix as in decomposition (<code>A[i, k] = Z_k(\u03c1_i, \u03b8_i)<\/code>)<\/td>\n<\/tr>\n<tr>\n<td><code>a<\/code><\/td>\n<td>9\u00d71<\/td>\n<td>fitted (or otherwise known) Zernike coefficient vector<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">Per measurement point i, the reconstructed thickness is the inner product of basis row i with the coefficient vector:<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"T_recon[i] = a_1 \u00b7 Z_1(\u03c1_i, \u03b8_i)\n           + a_2 \u00b7 Z_2(\u03c1_i, \u03b8_i)\n           + ...\n           + a_N \u00b7 Z_N(\u03c1_i, \u03b8_i)\n           = \u03a3_k  A[i, k] \u00b7 a_k\"><pre class=\"notranslate\"><code>T_recon[i] = a_1 \u00b7 Z_1(\u03c1_i, \u03b8_i)\n           + a_2 \u00b7 Z_2(\u03c1_i, \u03b8_i)\n           + ...\n           + a_N \u00b7 Z_N(\u03c1_i, \u03b8_i)\n           = \u03a3_k  A[i, k] \u00b7 a_k\n<\/code><\/pre><\/div>\n<p dir=\"auto\">Stacking all 13 rows gives <code>T_recon = A \u00b7 a<\/code>. For many wafers at once, the implementation runs one matrix multiply <code>T_matrix = a_matrix @ A.T<\/code> so the output shape is <code>(n_wafers, n_points)<\/code>.<\/p>\n<p dir=\"auto\"><strong>Tiny worked example<\/strong> (illustrative A values, not the real Zernike values; n_terms=3, n_points=4):<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"a = [500.0, 2.0, 0.5]       # piston, tilt-x, defocus\nA = [[1.0,  0.0, -1.0],     # row per measurement point\n     [1.0,  1.0,  0.0],\n     [1.0,  0.0,  1.0],\n     [1.0, -1.0,  0.0]]\nT_recon = A \u00b7 a\n        = [1.0\u00b7500 + 0.0\u00b72 + (-1.0)\u00b70.5,    # = 499.5\n           1.0\u00b7500 + 1.0\u00b72 +  0.0\u00b70.5,      # = 502.0\n           1.0\u00b7500 + 0.0\u00b72 +  1.0\u00b70.5,      # = 500.5\n           1.0\u00b7500 + (-1.0)\u00b72 + 0.0\u00b70.5]    # = 498.0\"><pre class=\"notranslate\"><code>a = [500.0, 2.0, 0.5]       # piston, tilt-x, defocus\nA = [[1.0,  0.0, -1.0],     # row per measurement point\n     [1.0,  1.0,  0.0],\n     [1.0,  0.0,  1.0],\n     [1.0, -1.0,  0.0]]\nT_recon = A \u00b7 a\n        = [1.0\u00b7500 + 0.0\u00b72 + (-1.0)\u00b70.5,    # = 499.5\n           1.0\u00b7500 + 1.0\u00b72 +  0.0\u00b70.5,      # = 502.0\n           1.0\u00b7500 + 0.0\u00b72 +  1.0\u00b70.5,      # = 500.5\n           1.0\u00b7500 + (-1.0)\u00b72 + 0.0\u00b70.5]    # = 498.0\n<\/code><\/pre><\/div>\n<p dir=\"auto\"><strong>Comparison with decomposition<\/strong>:<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Direction<\/th>\n<th>Knowns<\/th>\n<th>Unknown<\/th>\n<th>Cost<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Decompose (Stage 2)<\/td>\n<td>T (measured) + A (geometry)<\/td>\n<td>a \u2014 recover via LSQ \/ Ridge<\/td>\n<td>inversion \/ regularization per wafer<\/td>\n<\/tr>\n<tr>\n<td>Reconstruct (<code>wlzpoly.reconstruct<\/code>)<\/td>\n<td>a (fitted) + A (geometry)<\/td>\n<td>T_recon \u2014 compute directly<\/td>\n<td>one matrix multiply (no fitting)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">Reconstruction never recovers the original noise \u03b5; the residual <code>T \u2212 T_recon<\/code> is the part the first N Zernike terms could not absorb.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Fitting<\/h3><a id=\"user-content-fitting\" class=\"anchor\" aria-label=\"Permalink: Fitting\" href=\"#fitting\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Solver<\/th>\n<th>Formula<\/th>\n<th>Properties<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>LSQ<\/strong><\/td>\n<td>\u00e2 = (A\u1d40A)\u207b\u00b9 A\u1d40 T<\/td>\n<td>Unbiased, higher variance<\/td>\n<\/tr>\n<tr>\n<td><strong>Ridge<\/strong><\/td>\n<td>\u00e2 = (A\u1d40A + \u03bbI)\u207b\u00b9 A\u1d40 T<\/td>\n<td>Biased toward zero, lower variance<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">The hat on <strong>\u00e2<\/strong> is the standard statistics convention for <em>estimate of the unknown true value<\/em> \u2014 here, the estimate of the true coefficient vector <code>a<\/code> recovered from the measurements.<\/p>\n<p dir=\"auto\">The CLI flag <code>--lam<\/code> directly supplies \u03bb in the Ridge formula (i.e. <strong><code>--lam<\/code> = \u03bb<\/strong>). For Ridge, \u03bb can be fixed via <code>--lam<\/code> or chosen automatically with <code>--auto_lam<\/code> (see next section).<\/p>\n<p dir=\"auto\"><strong>LSQ derivation<\/strong> \u2014 minimize the residual sum of squares:<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"J(a) = \u2016T \u2212 A\u00b7a\u2016\u00b2  =  (T \u2212 A\u00b7a)\u1d40 (T \u2212 A\u00b7a)\n\n\u2202J\/\u2202a = \u22122 A\u1d40 (T \u2212 A\u00b7a) = 0\n      \u21d2  A\u1d40 A \u00b7 a = A\u1d40 T\n      \u21d2  \u00e2 = (A\u1d40 A)\u207b\u00b9 A\u1d40 T\"><pre class=\"notranslate\"><code>J(a) = \u2016T \u2212 A\u00b7a\u2016\u00b2  =  (T \u2212 A\u00b7a)\u1d40 (T \u2212 A\u00b7a)\n\n\u2202J\/\u2202a = \u22122 A\u1d40 (T \u2212 A\u00b7a) = 0\n      \u21d2  A\u1d40 A \u00b7 a = A\u1d40 T\n      \u21d2  \u00e2 = (A\u1d40 A)\u207b\u00b9 A\u1d40 T\n<\/code><\/pre><\/div>\n<p dir=\"auto\"><strong>Ridge derivation<\/strong> \u2014 same loss as LSQ plus a coefficient-size penalty:<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"J(a) = \u2016T \u2212 A\u00b7a\u2016\u00b2 + \u03bb \u2016a\u2016\u00b2\n\n\u2202J\/\u2202a = \u22122 A\u1d40 (T \u2212 A\u00b7a) + 2 \u03bb a = 0\n      \u21d2  (A\u1d40 A + \u03bbI) \u00b7 a = A\u1d40 T\n      \u21d2  \u00e2 = (A\u1d40 A + \u03bbI)\u207b\u00b9 A\u1d40 T\"><pre class=\"notranslate\"><code>J(a) = \u2016T \u2212 A\u00b7a\u2016\u00b2 + \u03bb \u2016a\u2016\u00b2\n\n\u2202J\/\u2202a = \u22122 A\u1d40 (T \u2212 A\u00b7a) + 2 \u03bb a = 0\n      \u21d2  (A\u1d40 A + \u03bbI) \u00b7 a = A\u1d40 T\n      \u21d2  \u00e2 = (A\u1d40 A + \u03bbI)\u207b\u00b9 A\u1d40 T\n<\/code><\/pre><\/div>\n<p dir=\"auto\">The only structural difference is the <code>\u03bbI<\/code> added on the diagonal of <code>A\u1d40A<\/code>, which (a) makes the matrix invertible even when <code>A\u1d40A<\/code> is rank-deficient, and (b) shrinks the coefficients toward zero (bias) in exchange for lower variance.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">LOOCV-based \u03bb selection<\/h3><a id=\"user-content-loocv-based-\u03bb-selection\" class=\"anchor\" aria-label=\"Permalink: LOOCV-based \u03bb selection\" href=\"#loocv-based-\u03bb-selection\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<p dir=\"auto\">When <code>wlzpoly.decompose --solver ridge --auto_lam<\/code> is set, \u03bb is picked by <strong>Leave-One-Out Cross-Validation<\/strong> instead of being supplied as a fixed <code>--lam<\/code>.<\/p>\n<p dir=\"auto\"><strong>Inputs<\/strong>:<\/p>\n<ul dir=\"auto\">\n<li><code>A<\/code> \u2014 basis matrix (N measurement points \u00d7 n_terms columns)<\/li>\n<li><code>T<\/code> \u2014 a measurement vector for one wafer (length N)<\/li>\n<li><code>\u03bb<\/code> candidates \u2014 <code>--loocv_lambdas<\/code> (default <code>[0.0, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0]<\/code>)<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Algorithm<\/strong> (for each candidate \u03bb):<\/p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"for i in 0..N-1:                              # N folds\n    A_train = A with row i removed            # (N-1) \u00d7 n_terms\n    T_train = T with element i removed        # (N-1)\n    \u00e2 = (A_train\u1d40 A_train + \u03bbI)\u207b\u00b9 A_train\u1d40 T_train\n    pred_i  = A[i, :] @ \u00e2                     # predict the held-out point\n    err_i   = T[i] - pred_i\n\nmean_err\u00b2(\u03bb) = mean( err_i\u00b2 for i in 0..N-1 )\n\nbest_\u03bb = argmin_\u03bb  mean_err\u00b2(\u03bb)\"><pre class=\"notranslate\"><code>for i in 0..N-1:                              # N folds\n    A_train = A with row i removed            # (N-1) \u00d7 n_terms\n    T_train = T with element i removed        # (N-1)\n    \u00e2 = (A_train\u1d40 A_train + \u03bbI)\u207b\u00b9 A_train\u1d40 T_train\n    pred_i  = A[i, :] @ \u00e2                     # predict the held-out point\n    err_i   = T[i] - pred_i\n\nmean_err\u00b2(\u03bb) = mean( err_i\u00b2 for i in 0..N-1 )\n\nbest_\u03bb = argmin_\u03bb  mean_err\u00b2(\u03bb)\n<\/code><\/pre><\/div>\n<p dir=\"auto\">Each \u03bb candidate is scored by how well an N-1 fit predicts the left-out point, averaged over all N folds. Small \u03bb \u2192 overfits noise, leave-out predictions bad. Large \u03bb \u2192 kills signal, leave-out predictions bad. The minimum-error \u03bb is the sweet spot.<\/p>\n<p dir=\"auto\"><strong>Which T to scan over<\/strong> \u2014 <code>--loocv_ref<\/code> (decompose-only):<\/p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Mode<\/th>\n<th>Behavior<\/th>\n<th>When useful<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>first_wafer<\/code> (default)<\/td>\n<td>LOOCV on the first wafer&#8217;s T; that single \u03bb is reused for all wafers<\/td>\n<td>Fast; first wafer is representative of the batch<\/td>\n<\/tr>\n<tr>\n<td><code>mean<\/code><\/td>\n<td>Per-point mean across all wafers; single \u03bb for all<\/td>\n<td>Single outlier wafer shouldn&#8217;t dominate \u03bb choice<\/td>\n<\/tr>\n<tr>\n<td><code>per_wafer<\/code><\/td>\n<td>Run LOOCV per wafer; each wafer gets its own \u03bb<\/td>\n<td>Most accurate, N\u00d7 slower; wafers have very different noise<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\"><strong>Effect on coefficients<\/strong>: in the bundled demo (36 wafers, \u03c3=5.0 noise) LOOCV consistently picks <strong>\u03bb = 0.01<\/strong>, which is the same value as the fixed <code>--lam<\/code> default \u2014 so LSQ and Ridge RMSE are within 5%. With higher noise (\u03c3 \u2265 10) Ridge with LOOCV-picked \u03bb noticeably outperforms LSQ on edge-of-disk coefficients (a\u2087..a\u2089).<\/p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">W2W \/ WiW separation<\/h3><a id=\"user-content-w2w--wiw-separation\" class=\"anchor\" aria-label=\"Permalink: W2W \/ WiW separation\" href=\"#w2w--wiw-separation\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Group<\/th>\n<th>Coefficient(s)<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>W2W<\/strong> (Wafer-to-Wafer)<\/td>\n<td>a\u2081 (Piston)<\/td>\n<td>Mean thickness<\/td>\n<\/tr>\n<tr>\n<td><strong>WiW<\/strong> (Within-Wafer)<\/td>\n<td>a\u2082..a\u2089<\/td>\n<td>Spatial pattern (8 modes)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/markdown-accessiblity-table>\n<p dir=\"auto\">Sanity check: 13 measurements \u2192 9 coefficients \u2192 4 residual DOF (used for residual monitoring).<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Dependencies<\/h2><a id=\"user-content-dependencies\" class=\"anchor\" aria-label=\"Permalink: Dependencies\" href=\"#dependencies\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"Python 3.9+\nnumpy &gt;= 1.22\npandas &gt;= 1.5\nmatplotlib &gt;= 3.5\ntqdm &gt;= 4.60\"><pre class=\"notranslate\"><code>Python 3.9+\nnumpy &gt;= 1.22\npandas &gt;= 1.5\nmatplotlib &gt;= 3.5\ntqdm &gt;= 4.60\n<\/code><\/pre><\/div>\n<p dir=\"auto\">Standard-library only beyond those: <code>argparse<\/code>, <code>csv<\/code>, <code>json<\/code>, <code>math<\/code>, <code>pathlib<\/code>, <code>typing<\/code>.<\/p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Links<\/h2><a id=\"user-content-links\" class=\"anchor\" aria-label=\"Permalink: Links\" href=\"#links\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"><\/path><\/svg><\/a><\/div>\n<ul dir=\"auto\">\n<li><strong>PyPI<\/strong>: <a href=\"https:\/\/pypi.org\/project\/wlzpoly\/\" rel=\"nofollow noopener\" target=\"_blank\">https:\/\/pypi.org\/project\/wlzpoly\/<\/a><\/li>\n<li><strong>Source<\/strong>: <a href=\"https:\/\/github.com\/ykim2718\/WaferLevelZernikePolynomials\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/ykim2718\/WaferLevelZernikePolynomials<\/a><\/li>\n<li><strong>Issues<\/strong>: <a href=\"https:\/\/github.com\/ykim2718\/WaferLevelZernikePolynomials\/issues\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/ykim2718\/WaferLevelZernikePolynomials\/issues<\/a><\/li>\n<li><strong>License<\/strong>: MIT<\/li>\n<\/ul>\n<\/article><\/div><\/div><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n<div style='text-align:center' class='yasr-auto-insert-overall'><\/div><div style='text-align:center' class='yasr-auto-insert-visitor'><\/div>","protected":false},"excerpt":{"rendered":"<p>wlzpoly is a Python package that decomposes N-point wafer thickness measurements into M Zernike polynomial coefficients using LSQ or Ridge regression with LOOCV-tuned regularization. It ships with a reproducible three-stage demo (synthetic data, coefficient fitting, ground-truth verification), per-stage CLI, and a clean pure-Python API for semiconductor wafer-level process metrology.<\/p>\n","protected":false},"author":4,"featured_media":6671,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"_kadence_starter_templates_imported_post":false,"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","yasr_overall_rating":0,"yasr_post_is_review":"","yasr_auto_insert_disabled":"","yasr_review_type":"","fifu_image_url":"","fifu_image_alt":"","iawp_total_views":24,"footnotes":""},"categories":[56,373,374],"tags":[],"class_list":["post-6660","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-data-science-slug","category-feature-engineering-slug","category-label-engineering-slug"],"yasr_visitor_votes":{"stars_attributes":{"read_only":false,"span_bottom":false},"number_of_votes":1,"sum_votes":5},"jetpack_featured_media_url":"https:\/\/ykim.synology.me\/wordpress\/wp-content\/uploads\/2026\/05\/zernike_pyramid-1200x800px.png","_links":{"self":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts\/6660","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/comments?post=6660"}],"version-history":[{"count":8,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts\/6660\/revisions"}],"predecessor-version":[{"id":6674,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts\/6660\/revisions\/6674"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/media\/6671"}],"wp:attachment":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/media?parent=6660"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/categories?post=6660"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/tags?post=6660"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}